#!/usr/bin/env python3
from __future__ import annotations

import json
import os
import tempfile
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Dict, Optional


def now_iso() -> str:
    return datetime.now().astimezone().isoformat(timespec="seconds")


def parse_iso(value: str) -> Optional[datetime]:
    text = str(value or "").strip()
    if not text:
        return None
    try:
        parsed = datetime.fromisoformat(text)
        if parsed.tzinfo is None:
            return parsed.replace(tzinfo=datetime.now().astimezone().tzinfo)
        return parsed
    except Exception:
        return None


def resolve_user_data_root(*, explicit: str | Path | None = None, helper_file: str | Path | None = None) -> Path:
    if explicit:
        return Path(explicit).expanduser().resolve()

    for env_key in ("CHEAPCLAW_USER_DATA_ROOT", "MLA_USER_DATA_ROOT"):
        env_root = os.environ.get(env_key, "").strip()
        if env_root:
            return Path(env_root).expanduser().resolve()

    helper_parent = Path(helper_file).resolve().parent if helper_file else Path(__file__).resolve().parent
    if (helper_parent / "cheapclaw").exists():
        return helper_parent

    return (Path.home() / "mla_v3").resolve()


def slugify(value: str, fallback: str = "item", max_len: int = 80) -> str:
    text = str(value or "").strip()
    if not text:
        return fallback
    chars = []
    last_dash = False
    for ch in text:
        if ch.isalnum():
            chars.append(ch.lower())
            last_dash = False
        elif ch in {"-", "_"}:
            chars.append(ch)
            last_dash = False
        else:
            if not last_dash:
                chars.append("-")
            last_dash = True
    return "".join(chars).strip("-_")[:max_len] or fallback


def is_relative_to(path: Path, parent: Path) -> bool:
    try:
        path.relative_to(parent)
        return True
    except ValueError:
        return False


def atomic_write_text(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp_name = tempfile.mkstemp(prefix=path.name + ".", dir=str(path.parent))
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as tmp_file:
            tmp_file.write(content)
        os.replace(tmp_name, path)
    finally:
        if os.path.exists(tmp_name):
            os.unlink(tmp_name)


def atomic_write_json(path: Path, payload: Dict[str, Any]) -> None:
    atomic_write_text(path, json.dumps(payload, ensure_ascii=False, indent=2))


def load_json(path: Path, default: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return default


@dataclass(frozen=True)
class CheapClawPaths:
    user_data_root: Path
    cheapclaw_root: Path
    tools_root: Path
    panel_dir: Path
    panel_path: Path
    panel_lock_path: Path
    panel_backups_dir: Path
    plans_path: Path
    config_dir: Path
    app_config_path: Path
    app_config_example_path: Path
    channels_config_path: Path
    channels_example_path: Path
    channels_root: Path
    outbox_dir: Path
    tasks_root: Path
    task_skills_root: Path
    supervisor_task_id: Path
    monitor_instructions_path: Path
    monitor_system_add_path: Path
    runtime_dir: Path
    runtime_state_path: Path

    @classmethod
    def from_user_data_root(cls, user_data_root: str | Path, app_name: str = "cheapclaw") -> "CheapClawPaths":
        root = Path(user_data_root).expanduser().resolve()
        cheapclaw_root = root / app_name
        panel_dir = cheapclaw_root / "panel"
        runtime_dir = cheapclaw_root / "runtime"
        config_dir = cheapclaw_root / "config"
        return cls(
            user_data_root=root,
            cheapclaw_root=cheapclaw_root,
            tools_root=root / "tools_library",
            panel_dir=panel_dir,
            panel_path=panel_dir / "panel.json",
            panel_lock_path=panel_dir / "panel.lock",
            panel_backups_dir=panel_dir / "backups",
            plans_path=cheapclaw_root / "plans.json",
            config_dir=config_dir,
            app_config_path=config_dir / "app_config.json",
            app_config_example_path=config_dir / "app_config.example.json",
            channels_config_path=config_dir / "channels.json",
            channels_example_path=config_dir / "channels.example.json",
            channels_root=cheapclaw_root / "channels",
            outbox_dir=cheapclaw_root / "outbox",
            tasks_root=cheapclaw_root / "tasks",
            task_skills_root=cheapclaw_root / "task_skills",
            supervisor_task_id=cheapclaw_root / "supervisor_task",
            monitor_instructions_path=cheapclaw_root / "monitor_instructions.json",
            monitor_system_add_path=cheapclaw_root / "supervisor_task" / "system-add.md",
            runtime_dir=runtime_dir,
            runtime_state_path=runtime_dir / "state.json",
        )


def _default_panel(paths: CheapClawPaths) -> Dict[str, Any]:
    return {
        "version": 1,
        "channels": {},
        "service_state": {
            "main_agent_task_id": str(paths.supervisor_task_id),
            "main_agent_running": False,
            "main_agent_run_id": "",
            "main_agent_last_started_at": "",
            "main_agent_last_finished_at": "",
            "watchdog_last_run_at": "",
            "last_backup_path": "",
        },
    }


def _normalize_conversation(paths: CheapClawPaths, channel: str, conversation_id: str, conversation: Dict[str, Any]) -> Dict[str, Any]:
    if not isinstance(conversation, dict):
        conversation = {}
    defaults = {
        "channel": channel,
        "conversation_id": conversation_id,
        "conversation_type": "group",
        "display_name": conversation_id,
        "trigger_policy": {"require_mention": True},
        "message_history_path": str(paths.channels_root / slugify(channel) / slugify(conversation_id) / "social_history.jsonl"),
        "context_summary_path": str(paths.channels_root / slugify(channel) / slugify(conversation_id) / "latest_context.md"),
        "messages": [],
        "linked_tasks": [],
        "pending_events": [],
        "last_snapshot_path": "",
        "updated_at": "",
        "running_task_count": 0,
        "has_stale_running_tasks": False,
        "latest_user_message_at": "",
        "latest_bot_message_at": "",
        "unread_event_count": 0,
        "last_reply_summary": "",
        "conversation_tags": [],
        "message_task_bindings": [],
    }
    for key, value in defaults.items():
        conversation.setdefault(key, value)
    conversation.pop("dirty", None)
    normalized_tasks = []
    for item in conversation.get("linked_tasks", []):
        if not isinstance(item, dict):
            continue
        task_id = str(item.get("task_id") or "").strip()
        if not task_id:
            continue
        task_defaults = {
            "task_id": task_id,
            "created_at": "",
            "agent_system": "",
            "agent_name": "",
            "status": "unknown",
            "share_context_path": "",
            "stack_path": "",
            "log_path": "",
            "skills_dir": "",
            "default_exposed_skills": [],
            "mcp_servers": [],
            "last_thinking": "",
            "last_thinking_at": "",
            "last_final_output": "",
            "last_final_output_at": "",
            "last_action_at": "",
            "last_log_at": "",
            "fresh_retry_count": 0,
            "last_watchdog_note": "",
            "pid_alive": None,
            "watchdog_observation": "",
            "watchdog_suspected_state": "",
        }
        task_defaults.update(item)
        normalized_tasks.append(task_defaults)
    conversation["linked_tasks"] = normalized_tasks
    conversation["running_task_count"] = sum(1 for item in normalized_tasks if item.get("status") == "running")
    conversation["unread_event_count"] = len(conversation.get("pending_events", []))
    return conversation


def normalize_panel(paths: CheapClawPaths, panel: Dict[str, Any]) -> Dict[str, Any]:
    if not isinstance(panel, dict):
        panel = {}
    panel.setdefault("version", 1)
    panel.setdefault("channels", {})
    panel.setdefault("service_state", {})
    defaults = {
        "main_agent_task_id": str(paths.supervisor_task_id),
        "main_agent_running": False,
        "main_agent_run_id": "",
        "main_agent_last_started_at": "",
        "main_agent_last_finished_at": "",
        "watchdog_last_run_at": "",
        "last_backup_path": "",
    }
    for key, value in defaults.items():
        panel["service_state"].setdefault(key, value)
    panel["service_state"].pop("main_agent_dirty", None)
    for channel, payload in list(panel["channels"].items()):
        if not isinstance(payload, dict):
            panel["channels"][channel] = {"conversations": {}}
            payload = panel["channels"][channel]
        payload.setdefault("conversations", {})
        for conversation_id, conversation in list(payload["conversations"].items()):
            payload["conversations"][conversation_id] = _normalize_conversation(paths, channel, conversation_id, conversation)
    return panel


def normalize_monitor_instructions(payload: Dict[str, Any]) -> Dict[str, Any]:
    if not isinstance(payload, dict):
        payload = {"version": 1, "instructions": []}
    payload.setdefault("version", 1)
    items = payload.get("instructions")
    if not isinstance(items, list):
        items = []
    payload["instructions"] = [item for item in items if isinstance(item, dict)]
    return payload


def ensure_runtime_layout(paths: CheapClawPaths) -> None:
    for path in [
        paths.cheapclaw_root,
        paths.tools_root,
        paths.panel_dir,
        paths.panel_backups_dir,
        paths.config_dir,
        paths.runtime_dir,
        paths.channels_root,
        paths.outbox_dir,
        paths.tasks_root,
        paths.task_skills_root,
        paths.supervisor_task_id,
    ]:
        path.mkdir(parents=True, exist_ok=True)
    if not paths.panel_path.exists():
        atomic_write_json(paths.panel_path, _default_panel(paths))
    if not paths.plans_path.exists():
        atomic_write_json(paths.plans_path, {"version": 1, "plans": []})
    if not paths.runtime_state_path.exists():
        atomic_write_json(paths.runtime_state_path, {"webhook_server": {}, "telegram_offsets": {}})
    if not paths.monitor_instructions_path.exists():
        atomic_write_json(paths.monitor_instructions_path, {"version": 1, "instructions": []})


def _write_panel_backup(paths: CheapClawPaths) -> Path:
    backup_path = paths.panel_backups_dir / f"panel_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.json"
    backup_path.write_text(paths.panel_path.read_text(encoding="utf-8"), encoding="utf-8")
    return backup_path


def load_panel_for_paths(paths: CheapClawPaths) -> Dict[str, Any]:
    ensure_runtime_layout(paths)
    panel = load_json(paths.panel_path, _default_panel(paths))
    return normalize_panel(paths, panel)


def save_panel_for_paths(paths: CheapClawPaths, panel: Dict[str, Any], *, backup: bool = True) -> Dict[str, Any]:
    ensure_runtime_layout(paths)
    normalized = normalize_panel(paths, panel)
    if backup and paths.panel_path.exists():
        backup_path = _write_panel_backup(paths)
        normalized.setdefault("service_state", {})["last_backup_path"] = str(backup_path)
    atomic_write_json(paths.panel_path, normalized)
    return normalized


def mutate_panel_for_paths(
    paths: CheapClawPaths,
    updater: Callable[[Dict[str, Any]], Dict[str, Any] | None],
    *,
    backup: bool = True,
) -> Dict[str, Any]:
    current = load_panel_for_paths(paths)
    updated = updater(current)
    panel = current if updated is None else updated
    return save_panel_for_paths(paths, panel, backup=backup)


def load_monitor_instructions_from_path(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {"version": 1, "instructions": []}
    payload = load_json(path, {"version": 1, "instructions": []})
    return normalize_monitor_instructions(payload)


def save_monitor_instructions_to_path(path: Path, payload: Dict[str, Any]) -> Dict[str, Any]:
    normalized = normalize_monitor_instructions(payload)
    atomic_write_json(path, normalized)
    return normalized


def load_monitor_instructions_for_paths(paths: CheapClawPaths) -> Dict[str, Any]:
    ensure_runtime_layout(paths)
    return load_monitor_instructions_from_path(paths.monitor_instructions_path)


def save_monitor_instructions_for_paths(paths: CheapClawPaths, payload: Dict[str, Any]) -> Dict[str, Any]:
    ensure_runtime_layout(paths)
    return save_monitor_instructions_to_path(paths.monitor_instructions_path, payload)
