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

import json
import os
import re
import shutil
import sys
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional


REPO_ROOT = Path(__file__).resolve().parents[1]
SERVICE_PATH = REPO_ROOT / "cheapclaw_service.py"
TEMPLATE_MANIFEST_PATH = REPO_ROOT / "assets" / "config" / "fleet.manifest.example.json"
DEFAULT_HOME_ROOT = (Path.home() / "cheapclaw").resolve()
DEFAULT_RUNTIME_ROOT = (DEFAULT_HOME_ROOT / "runtime").resolve()
DEFAULT_MANIFEST_PATH = (DEFAULT_HOME_ROOT / "fleet.manifest.json").resolve()
PLACEHOLDER_SNIPPETS = (
    "/ABS/PATH/TO/",
    "/ABS/PATH/TO",
    "telegram_token_here",
    "cli_xxx",
)
TELEGRAM_TOKEN_PATTERN = re.compile(r"^\d{6,}:[A-Za-z0-9_-]{20,}$")
PROXY_ENV_KEYS = (
    "http_proxy",
    "https_proxy",
    "all_proxy",
    "no_proxy",
    "HTTP_PROXY",
    "HTTPS_PROXY",
    "ALL_PROXY",
    "NO_PROXY",
)


def _is_placeholder_text(text: str) -> bool:
    value = str(text or "").strip()
    if not value:
        return False
    if any(snippet in value for snippet in PLACEHOLDER_SNIPPETS):
        return True
    return value in {"xxx", "your_token_here", "your_app_id", "your_app_secret"}


def _child_process_env(manifest: Dict[str, Any]) -> Dict[str, str]:
    env = os.environ.copy()
    for key in PROXY_ENV_KEYS:
        env.pop(key, None)

    proxy_env = manifest.get("proxy_env")
    if not isinstance(proxy_env, dict):
        return env

    for key in ("http_proxy", "https_proxy", "all_proxy", "no_proxy"):
        value = str(proxy_env.get(key) or "").strip()
        if not value:
            continue
        env[key] = value
        env[key.upper()] = value
    return env


def _bot_seed_is_placeholder(bot: Dict[str, Any]) -> bool:
    channel = str(bot.get("channel") or "").strip().lower()
    if channel == "localweb":
        return False
    if channel == "telegram":
        block = bot.get("telegram") if isinstance(bot.get("telegram"), dict) else {}
        token = str(block.get("bot_token") or "").strip()
        return (not token) or _is_placeholder_text(token)
    if channel == "feishu":
        block = bot.get("feishu") if isinstance(bot.get("feishu"), dict) else {}
        app_id = str(block.get("app_id") or "").strip()
        app_secret = str(block.get("app_secret") or "").strip()
        return (not app_id) or (not app_secret) or _is_placeholder_text(app_id) or _is_placeholder_text(app_secret)
    if channel == "whatsapp":
        block = bot.get("whatsapp") if isinstance(bot.get("whatsapp"), dict) else {}
        access_token = str(block.get("access_token") or "").strip()
        phone_number_id = str(block.get("phone_number_id") or "").strip()
        verify_token = str(block.get("verify_token") or "").strip()
        return (
            (not access_token)
            or (not phone_number_id)
            or (not verify_token)
            or _is_placeholder_text(access_token)
            or _is_placeholder_text(phone_number_id)
            or _is_placeholder_text(verify_token)
        )
    if channel == "discord":
        block = bot.get("discord") if isinstance(bot.get("discord"), dict) else {}
        token = str(block.get("bot_token") or "").strip()
        return (not token) or _is_placeholder_text(token)
    if channel in {"qq", "wechat"}:
        block = bot.get(channel) if isinstance(bot.get(channel), dict) else {}
        api_base = str(block.get("onebot_api_base") or "").strip()
        return (not api_base) or _is_placeholder_text(api_base)
    return True


def _load_json(path: Path) -> Dict[str, Any]:
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception as exc:
        raise SystemExit(f"[ERROR] Failed to parse JSON: {path} ({exc})")
    if not isinstance(payload, dict):
        raise SystemExit(f"[ERROR] JSON root must be an object: {path}")
    return payload


def _dump_json(path: Path, payload: Dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def _disabled_channels() -> Dict[str, Any]:
    return {
        "telegram": {"enabled": False, "bot_token": "", "allowed_chats": []},
        "feishu": {"enabled": False, "mode": "long_connection", "app_id": "", "app_secret": "", "verify_token": "", "encrypt_key": ""},
        "whatsapp": {"enabled": False, "access_token": "", "phone_number_id": "", "verify_token": "", "api_version": "v21.0"},
        "discord": {"enabled": False, "bot_id": "", "display_name": "", "bot_token": "", "intents": 37377, "require_mention_in_guild": True},
        "qq": {
            "enabled": False,
            "bot_id": "",
            "display_name": "",
            "onebot_api_base": "http://127.0.0.1:5700",
            "onebot_access_token": "",
            "onebot_post_secret": "",
            "onebot_self_id": "",
            "require_mention_in_group": True,
        },
        "wechat": {
            "enabled": False,
            "bot_id": "",
            "display_name": "",
            "onebot_api_base": "http://127.0.0.1:5701",
            "onebot_access_token": "",
            "onebot_post_secret": "",
            "onebot_self_id": "",
            "require_mention_in_group": True,
        },
        "localweb": {"enabled": False, "bot_id": "", "display_name": "", "require_mention_in_group": True},
    }


def _required_text(value: Any, field_name: str, bot_id: str) -> str:
    text = str(value or "").strip()
    if not text:
        raise SystemExit(f"[ERROR] Missing '{field_name}' for bot '{bot_id}'")
    return text


def _build_channels_config(bot: Dict[str, Any]) -> Dict[str, Any]:
    channel = str(bot.get("channel") or "").strip().lower()
    bot_id = _required_text(bot.get("bot_id"), "bot_id", "<unknown>")
    display_name = str(bot.get("display_name") or bot_id).strip() or bot_id
    cfg = _disabled_channels()

    if channel == "telegram":
        block = bot.get("telegram") if isinstance(bot.get("telegram"), dict) else {}
        token = _required_text(block.get("bot_token"), "telegram.bot_token", bot_id)
        if not TELEGRAM_TOKEN_PATTERN.match(token):
            raise SystemExit(
                "[ERROR] Invalid telegram.bot_token format for bot "
                f"'{bot_id}'. Expected full BotFather token like "
                "'123456789:AA...'."
            )
        allowed_chats = block.get("allowed_chats")
        if not isinstance(allowed_chats, list):
            allowed_chats = []
        cfg["telegram"].update(
            {
                "enabled": True,
                "bot_id": bot_id,
                "display_name": display_name,
                "bot_token": token,
                "allowed_chats": [str(item) for item in allowed_chats if str(item).strip()],
            }
        )
        return cfg

    if channel == "feishu":
        block = bot.get("feishu") if isinstance(bot.get("feishu"), dict) else {}
        app_id = _required_text(block.get("app_id"), "feishu.app_id", bot_id)
        app_secret = _required_text(block.get("app_secret"), "feishu.app_secret", bot_id)
        verify_token = str(block.get("verify_token") or "").strip()
        encrypt_key = str(block.get("encrypt_key") or "").strip()
        mode = str(block.get("mode") or "long_connection").strip() or "long_connection"
        cfg["feishu"].update(
            {
                "enabled": True,
                "bot_id": bot_id,
                "display_name": display_name,
                "mode": mode,
                "app_id": app_id,
                "app_secret": app_secret,
                "verify_token": verify_token,
                "encrypt_key": encrypt_key,
            }
        )
        return cfg

    if channel == "whatsapp":
        block = bot.get("whatsapp") if isinstance(bot.get("whatsapp"), dict) else {}
        access_token = _required_text(block.get("access_token"), "whatsapp.access_token", bot_id)
        phone_number_id = _required_text(block.get("phone_number_id"), "whatsapp.phone_number_id", bot_id)
        verify_token = _required_text(block.get("verify_token"), "whatsapp.verify_token", bot_id)
        api_version = str(block.get("api_version") or "v21.0").strip() or "v21.0"
        cfg["whatsapp"].update(
            {
                "enabled": True,
                "bot_id": bot_id,
                "display_name": display_name,
                "access_token": access_token,
                "phone_number_id": phone_number_id,
                "verify_token": verify_token,
                "api_version": api_version,
            }
        )
        return cfg

    if channel == "discord":
        block = bot.get("discord") if isinstance(bot.get("discord"), dict) else {}
        bot_token = _required_text(block.get("bot_token"), "discord.bot_token", bot_id)
        intents = int(block.get("intents") or 37377)
        cfg["discord"].update(
            {
                "enabled": True,
                "bot_id": bot_id,
                "display_name": display_name,
                "bot_token": bot_token,
                "intents": intents,
                "require_mention_in_guild": bool(block.get("require_mention_in_guild", True)),
            }
        )
        return cfg

    if channel in {"qq", "wechat"}:
        block = bot.get(channel) if isinstance(bot.get(channel), dict) else {}
        onebot_api_base = _required_text(block.get("onebot_api_base"), f"{channel}.onebot_api_base", bot_id)
        cfg[channel].update(
            {
                "enabled": True,
                "bot_id": bot_id,
                "display_name": display_name,
                "onebot_api_base": onebot_api_base,
                "onebot_access_token": str(block.get("onebot_access_token") or "").strip(),
                "onebot_post_secret": str(block.get("onebot_post_secret") or "").strip(),
                "onebot_self_id": str(block.get("onebot_self_id") or "").strip(),
                "require_mention_in_group": bool(block.get("require_mention_in_group", True)),
            }
        )
        return cfg

    if channel == "localweb":
        block = bot.get("localweb") if isinstance(bot.get("localweb"), dict) else {}
        cfg["localweb"].update(
            {
                "enabled": True,
                "bot_id": bot_id,
                "display_name": display_name,
                "require_mention_in_group": bool(block.get("require_mention_in_group", True)),
            }
        )
        return cfg

    raise SystemExit(f"[ERROR] Unsupported channel '{channel}' for bot '{bot_id}'")


def _validate_webhook_ports(bots: List[Dict[str, Any]]) -> None:
    used: Dict[tuple[str, int], str] = {}
    for bot in bots:
        if not bool(bot.get("serve_webhooks", False)):
            continue
        host = str(bot.get("host") or "127.0.0.1").strip() or "127.0.0.1"
        port = int(bot.get("port") or 8765)
        key = (host, port)
        if key in used:
            raise SystemExit(
                f"[ERROR] Webhook port conflict: {host}:{port} used by both '{used[key]}' and '{bot.get('bot_id')}'"
            )
        used[key] = str(bot.get("bot_id") or "")


def _scaffold_manifest(manifest_path: Path, *, force: bool = False) -> bool:
    if not TEMPLATE_MANIFEST_PATH.exists():
        raise SystemExit(f"[ERROR] template manifest not found: {TEMPLATE_MANIFEST_PATH}")
    if manifest_path.exists() and not force:
        return False
    manifest_path.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(TEMPLATE_MANIFEST_PATH, manifest_path)
    return True


def _scan_placeholder_fields(value: Any, path: str = "root") -> List[str]:
    hits: List[str] = []
    if isinstance(value, dict):
        for key, item in value.items():
            hits.extend(_scan_placeholder_fields(item, f"{path}.{key}"))
        return hits
    if isinstance(value, list):
        for idx, item in enumerate(value):
            hits.extend(_scan_placeholder_fields(item, f"{path}[{idx}]"))
        return hits
    if isinstance(value, str):
        text = value.strip()
        if _is_placeholder_text(text):
            hits.append(path)
    return hits


def _normalize_model_name(model_name: str) -> str:
    text = str(model_name or "").strip()
    if not text:
        raise SystemExit("[ERROR] model name is required")
    return text if "/" in text else f"openai/{text}"


def _write_minimal_llm_config(
    llm_config_path: Path,
    *,
    base_url: str,
    api_key: str,
    model_name: str,
    multimodal: bool,
) -> None:
    model_full = _normalize_model_name(model_name)
    mm = "true" if multimodal else "false"
    content = (
        "# Auto-generated by scripts/fleet_one_click.py interactive setup.\n"
        "# You can add more models manually later.\n"
        "temperature: 0\n"
        "max_tokens: 0\n"
        "max_context_window: 500000\n"
        f"base_url: {json.dumps(str(base_url), ensure_ascii=False)}\n"
        f"api_key: {json.dumps(str(api_key), ensure_ascii=False)}\n"
        "timeout: 300\n"
        "stream_timeout: 30\n"
        "first_chunk_timeout: 30\n"
        "\n"
        "tool_choice:\n"
        "  execution: required\n"
        "  thinking: required\n"
        "  compressor: required\n"
        "  image_generation: required\n"
        "  read_figure: required\n"
        "\n"
        "models:\n"
        f"  - name: {json.dumps(model_full, ensure_ascii=False)}\n"
        "    default: true\n"
        "    tool_choice: required\n"
        "\n"
        "figure_models:\n"
        f"  - name: {json.dumps(model_full, ensure_ascii=False)}\n"
        "    default: true\n"
        "    tool_choice: required\n"
        "\n"
        "compressor_models:\n"
        f"  - name: {json.dumps(model_full, ensure_ascii=False)}\n"
        "    default: true\n"
        "    tool_choice: required\n"
        "\n"
        "read_figure_models:\n"
        f"  - name: {json.dumps(model_full, ensure_ascii=False)}\n"
        "    default: true\n"
        "    tool_choice: required\n"
        "\n"
        "thinking_models:\n"
        f"  - name: {json.dumps(model_full, ensure_ascii=False)}\n"
        "    default: true\n"
        "    tool_choice: required\n"
        "\n"
        f"multimodal: {mm}\n"
        f"compressor_multimodal: {mm}\n"
    )
    llm_config_path.parent.mkdir(parents=True, exist_ok=True)
    llm_config_path.write_text(content, encoding="utf-8")


def _normalize_context_settings(context: Any) -> Dict[str, int]:
    source = context if isinstance(context, dict) else {}
    return {
        "user_history_compress_threshold_tokens": max(0, int(source.get("user_history_compress_threshold_tokens", 1500) or 1500)),
        "structured_call_info_compress_threshold_agents": max(1, int(source.get("structured_call_info_compress_threshold_agents", 10) or 10)),
        "structured_call_info_compress_threshold_tokens": max(0, int(source.get("structured_call_info_compress_threshold_tokens", 2200) or 2200)),
    }


def _apply_context_settings_to_app_config(user_data_root: Path, context: Dict[str, int]) -> None:
    app_config_path = user_data_root / "cheapclaw" / "config" / "app_config.json"
    payload = _load_json(app_config_path) if app_config_path.exists() else {}
    context_cfg = payload.get("context") if isinstance(payload.get("context"), dict) else {}
    context_cfg.update(_normalize_context_settings(context))
    payload["context"] = context_cfg
    _dump_json(app_config_path, payload)


def prepare_from_manifest(
    manifest_path: Path,
    *,
    write_fleet_to: Path | None = None,
    run_capture: Callable[[List[str], Dict[str, str], bool], Any],
    restore_package_defaults: bool = False,
) -> Dict[str, Any]:
    manifest = _load_json(manifest_path)
    context_settings = _normalize_context_settings(manifest.get("context"))
    llm_config_path = Path(_required_text(manifest.get("llm_config_path"), "llm_config_path", "<manifest>")).expanduser().resolve()
    if not llm_config_path.exists():
        raise SystemExit(f"[ERROR] llm_config_path does not exist: {llm_config_path}")

    runtime_root = Path(_required_text(manifest.get("runtime_root"), "runtime_root", "<manifest>")).expanduser().resolve()
    bots_raw = manifest.get("bots")
    if not isinstance(bots_raw, list) or not bots_raw:
        raise SystemExit("[ERROR] 'bots' must be a non-empty list")

    enabled_bots: List[Dict[str, Any]] = []
    seen_bot_ids = set()
    for item in bots_raw:
        if not isinstance(item, dict):
            continue
        if not bool(item.get("enabled", True)):
            continue
        bot_id = _required_text(item.get("bot_id"), "bot_id", "<manifest>")
        if bot_id in seen_bot_ids:
            raise SystemExit(f"[ERROR] Duplicate bot_id: {bot_id}")
        seen_bot_ids.add(bot_id)
        enabled_bots.append(item)
    if not enabled_bots:
        raise SystemExit("[ERROR] no enabled bots in manifest")

    _validate_webhook_ports(enabled_bots)

    fleet_config_path = (
        write_fleet_to.expanduser().resolve()
        if write_fleet_to
        else Path(str(manifest.get("fleet_config_path") or runtime_root / "fleet.generated.json")).expanduser().resolve()
    )

    env = _child_process_env(manifest)
    generated_bots: List[Dict[str, Any]] = []
    for item in enabled_bots:
        bot_id = str(item["bot_id"]).strip()
        display_name = str(item.get("display_name") or bot_id).strip() or bot_id
        user_data_root = (runtime_root / bot_id).resolve()
        channels_cfg = _build_channels_config(item)

        channels_path = user_data_root / "cheapclaw" / "config" / "channels.json"
        _dump_json(channels_path, channels_cfg)

        bootstrap_cmd = [
            sys.executable,
            str(SERVICE_PATH),
            "--user-data-root",
            str(user_data_root),
            "--llm-config-path",
            str(llm_config_path),
            "--bot-id",
            bot_id,
            "--bot-display-name",
            display_name,
            "--fleet-config-path",
            str(fleet_config_path),
            "--bootstrap",
        ]
        if restore_package_defaults:
            bootstrap_cmd.append("--restore-package-defaults")
        run_capture(bootstrap_cmd, env=env, check=True)
        _apply_context_settings_to_app_config(user_data_root, context_settings)

        fleet_item = {
            "bot_id": bot_id,
            "display_name": display_name,
            "enabled": True,
            "user_data_root": str(user_data_root),
            "llm_config_path": str(llm_config_path),
            "serve_webhooks": bool(item.get("serve_webhooks", False)),
        }
        if fleet_item["serve_webhooks"]:
            fleet_item["host"] = str(item.get("host") or "127.0.0.1")
            fleet_item["port"] = int(item.get("port") or 8765)
        generated_bots.append(fleet_item)

    fleet_payload = {"version": 1, "bots": generated_bots}
    _dump_json(fleet_config_path, fleet_payload)
    return {
        "status": "success",
        "manifest_path": str(manifest_path),
        "runtime_root": str(runtime_root),
        "fleet_config_path": str(fleet_config_path),
        "enabled_bot_count": len(generated_bots),
        "bots": generated_bots,
        "restore_package_defaults": bool(restore_package_defaults),
    }
