from __future__ import annotations

import json
import re
import subprocess
import sys
import time
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple

from rich.console import Console

from armorer.config import ConfigManager
from armorer.messaging.telegram import TelegramGateway
from armorer.security.conversation_guard import ConversationGuard


def user_guard(config_dir: Path) -> ConversationGuard:
    return ConversationGuard(config_dir=config_dir)


def proactive_settings(config_dir: Path) -> Dict[str, Any]:
    return user_guard(config_dir).get_proactive_security_settings()


def inspect_named_container(name: str) -> Dict[str, Any]:
    result = subprocess.run(
        [
            "docker",
            "ps",
            "-a",
            "--filter",
            f"name=^{name}$",
            "--format",
            "{{.Names}}\t{{.Status}}",
        ],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        return {"exists": False, "running": False, "status": ""}
    for raw_line in (result.stdout or "").splitlines():
        line = raw_line.strip()
        if not line:
            continue
        container_name, _, status = line.partition("\t")
        if container_name.strip() != name:
            continue
        status_text = status.strip()
        return {
            "exists": True,
            "running": status_text.startswith("Up"),
            "status": status_text,
        }
    return {"exists": False, "running": False, "status": ""}


def load_recent_telegram_targets_cache(
    home_dir: Optional[Path] = None,
    *,
    min_updated_at: Optional[float] = None,
) -> List[Dict[str, str]]:
    home = Path(home_dir) if home_dir is not None else Path.home()
    cache_path = home / ".armorer" / "telegram_recent_targets.json"
    if not cache_path.exists():
        return []
    try:
        raw = json.loads(cache_path.read_text(encoding="utf-8"))
    except Exception:
        return []
    if not isinstance(raw, list):
        return []

    targets: List[Dict[str, str]] = []
    for row in raw:
        if not isinstance(row, dict):
            continue
        try:
            updated_at = float(row.get("updated_at") or 0.0)
        except Exception:
            updated_at = 0.0
        if min_updated_at is not None and updated_at < float(min_updated_at):
            continue
        chat_id = str(row.get("chat_id") or "").strip()
        if not chat_id:
            continue
        target = {
            "chat_id": chat_id,
            "chat_type": str(row.get("chat_type") or "").strip(),
            "title": str(row.get("title") or chat_id).strip(),
        }
        user_id = str(row.get("user_id") or "").strip()
        if user_id:
            target["user_id"] = user_id
        targets.append(target)
    return targets


def wait_for_telegram_targets_from_live_bridge(
    started_at: float,
    *,
    timeout_seconds: float = 20.0,
    poll_interval_seconds: float = 1.0,
    load_recent_targets_cache: Callable[..., List[Dict[str, str]]] = load_recent_telegram_targets_cache,
    time_now: Callable[[], float] = time.time,
    sleep_fn: Callable[[float], None] = time.sleep,
) -> Tuple[List[Dict[str, str]], bool]:
    deadline = float(started_at) + max(0.0, float(timeout_seconds or 0.0))
    while time_now() <= deadline:
        fresh = load_recent_targets_cache(min_updated_at=started_at)
        if fresh:
            return fresh, True
        sleep_fn(max(0.1, float(poll_interval_seconds or 0.0)))
    return load_recent_targets_cache(min_updated_at=None), False


def recommended_admin_chat_ids(
    *,
    alert_chat: str,
    current_admin_chat_ids: List[str],
    discovered_targets: List[Dict[str, str]],
) -> List[str]:
    discovered_chat_ids = {
        str(target.get("chat_id") or "").strip()
        for target in discovered_targets
        if str(target.get("chat_id") or "").strip()
    }
    defaults: List[str] = []
    alert_chat_id = str(alert_chat or "").strip()
    if alert_chat_id:
        defaults.append(alert_chat_id)
    for value in current_admin_chat_ids or []:
        chat_id = str(value or "").strip()
        if not chat_id or chat_id in defaults:
            continue
        if chat_id in discovered_chat_ids:
            defaults.append(chat_id)
    return defaults


def recommended_admin_user_ids(
    *,
    current_admin_user_ids: List[str],
    discovered_targets: List[Dict[str, str]],
    selected_admin_chat_ids: List[str],
) -> List[str]:
    selected_ids = {str(item or "").strip() for item in selected_admin_chat_ids if str(item or "").strip()}
    defaults: List[str] = []
    for target in discovered_targets:
        chat_id = str(target.get("chat_id") or "").strip()
        user_id = str(target.get("user_id") or "").strip()
        if not user_id or chat_id not in selected_ids or user_id in defaults:
            continue
        defaults.append(user_id)
    if defaults:
        return defaults
    for value in current_admin_user_ids or []:
        user_id = str(value or "").strip()
        if user_id and user_id not in defaults:
            defaults.append(user_id)
    return defaults


def has_advanced_telegram_admin_settings(
    *,
    current_admin_chat_ids: List[str],
    current_admin_user_ids: List[str],
    recommended_admin_chat_ids: List[str],
) -> bool:
    normalized_current = [str(item or "").strip() for item in current_admin_chat_ids if str(item or "").strip()]
    normalized_recommended = [str(item or "").strip() for item in recommended_admin_chat_ids if str(item or "").strip()]
    normalized_users = [str(item or "").strip() for item in current_admin_user_ids if str(item or "").strip()]
    return normalized_current != normalized_recommended or bool(normalized_users)


def proactive_security_subject(trigger_text: str, target_agent: Optional[str] = None) -> str:
    explicit = str(target_agent or "").strip()
    if explicit:
        return explicit
    text = str(trigger_text or "").strip()
    match = re.search(r"\b(?:install|run|configure|setup|start|stop|uninstall)\s+([A-Za-z0-9_.-]+)", text, re.I)
    if match:
        return match.group(1)
    words = [token for token in re.split(r"\s+", text) if token]
    return words[-1] if words else "this agent"


def maybe_onboard_proactive_security(
    cfg: ConfigManager,
    trigger_text: str,
    *,
    target_agent: Optional[str] = None,
    armorer_config_dir: Callable[[], Path],
    get_user_guard: Callable[..., ConversationGuard],
    ui_confirm: Callable[[str, bool], bool],
    configure_armorer_telegram: Callable[[ConfigManager], None],
) -> Dict[str, Any]:
    guard = get_user_guard()
    current = guard.get_proactive_security_settings()
    enabled = bool(current.get("proactive_security_enabled", False))
    prompted = bool(current.get("proactive_security_prompted", False))
    has_token = bool((cfg.find_vault_key("TELEGRAM_BOT_TOKEN") or "").strip())
    has_alert_chat = bool(str(current.get("telegram_alert_chat_id") or "").strip())

    if enabled and (not has_token or not has_alert_chat):
        configure_armorer_telegram(cfg)
        return guard.get_proactive_security_settings()

    if prompted or not sys.stdin.isatty():
        return current

    subject = proactive_security_subject(trigger_text, target_agent=target_agent)
    enable = ui_confirm(
        f"Enable proactive security for {subject}? Telegram alerts will notify you when Armorer detects risky behavior.",
        True,
    )
    updates = {
        "proactive_security_prompted": True,
        "proactive_security_enabled": bool(enable),
    }
    saved = guard.save_proactive_security_settings(updates)
    if enable:
        configure_armorer_telegram(cfg)
        return guard.get_proactive_security_settings()
    return saved


def print_telegram_bot_setup_steps(console: Console, bot_username: str = "") -> None:
    bot_handle = f"@{bot_username}" if bot_username else "your bot"
    console.print(f"[dim]Send a message to {bot_handle} so Armorer can discover your recent Telegram chat.[/dim]")


def configure_armorer_telegram(
    cfg: ConfigManager,
    *,
    console: Console,
    armorer_config_dir: Callable[[], Path],
    get_user_guard: Callable[..., ConversationGuard],
    ui_confirm: Callable[[str, bool], bool],
    ui_select: Callable[..., Any],
    ui_text: Callable[[str, Optional[str]], Optional[str]],
    ui_choice: Callable[..., Any],
    ui_choice_value: Callable[[Any], Any],
    inspect_named_container_fn: Callable[[str], Dict[str, Any]],
    load_recent_targets_cache: Callable[..., List[Dict[str, str]]],
    wait_for_live_targets: Callable[..., Tuple[List[Dict[str, str]], bool]],
    print_setup_steps: Callable[..., None],
    gateway_cls: type[TelegramGateway],
    has_advanced_settings_fn: Callable[..., bool],
    recommended_admin_chat_ids_fn: Callable[..., List[str]],
    recommended_admin_user_ids_fn: Callable[..., List[str]],
) -> Dict[str, Any]:
    existing_token = str(cfg.find_vault_key("TELEGRAM_BOT_TOKEN") or "").strip()
    token = existing_token or str(ui_text("Enter TELEGRAM_BOT_TOKEN", default=None) or "").strip()
    if not token:
        raise SystemExit(1)

    cfg.save_to_vault(
        "TELEGRAM_BOT_TOKEN",
        token,
        description="Telegram bot token used by Armorer for proactive security alerts.",
        aliases=["ARMORER_TELEGRAM_BOT_TOKEN"],
    )

    gateway = gateway_cls(bot_token=token)
    bot = gateway.get_me() or {}
    bot_username = str(bot.get("username") or "").strip()
    try:
        print_setup_steps(bot_username)
    except TypeError:
        print_setup_steps()

    core_state = inspect_named_container_fn("armorer-core")
    discovered_targets: List[Dict[str, str]] = []
    started_at = time.time()
    if bool(core_state.get("running")):
        discovered_targets = load_recent_targets_cache(min_updated_at=started_at)
        if not discovered_targets:
            discovered_targets, _ = wait_for_live_targets(started_at=started_at, timeout_seconds=15.0)
    if not discovered_targets:
        discovered_targets = load_recent_targets_cache(min_updated_at=None)
    if not discovered_targets:
        raise SystemExit(1)

    choice_items = [
        ui_choice(
            f"{target.get('title') or target.get('chat_id')} ({target.get('chat_type') or 'chat'} · {target.get('chat_id')})",
            target,
        )
        for target in discovered_targets
    ]
    selected = ui_select("Select the Telegram chat to receive alerts", choice_items)
    selected_target = ui_choice_value(selected)
    if not isinstance(selected_target, dict):
        raise SystemExit(1)

    guard = get_user_guard()
    current = guard.get_proactive_security_settings()
    alert_chat_id = str(selected_target.get("chat_id") or "").strip()
    current_admin_chat_ids = [str(item or "").strip() for item in current.get("telegram_admin_chat_ids", []) or []]
    current_admin_user_ids = [str(item or "").strip() for item in current.get("telegram_admin_user_ids", []) or []]

    recommended_chats = recommended_admin_chat_ids_fn(
        alert_chat=alert_chat_id,
        current_admin_chat_ids=current_admin_chat_ids,
        discovered_targets=discovered_targets,
    )
    recommended_users = recommended_admin_user_ids_fn(
        current_admin_user_ids=current_admin_user_ids,
        discovered_targets=discovered_targets,
        selected_admin_chat_ids=recommended_chats,
    )

    chat_id = str(selected_target.get("chat_id") or "").strip()
    user_id = str(selected_target.get("user_id") or "").strip()
    if (
        str(selected_target.get("chat_type") or "").strip().lower() == "private"
        and chat_id
        and user_id
        and chat_id == user_id
    ):
        console.print(
            "[dim]For this private chat, the detected user ID matches the chat ID. "
            "Armorer will use that ID for admin allowlists.[/dim]"
        )

    use_advanced = has_advanced_settings_fn(
        current_admin_chat_ids=current_admin_chat_ids,
        current_admin_user_ids=current_admin_user_ids,
        recommended_admin_chat_ids=recommended_chats,
    ) and ui_confirm("Configure advanced Telegram access controls?", False)

    admin_chat_ids = list(recommended_chats)
    admin_user_ids = list(recommended_users)
    allow_unconfigured_admins = bool(current.get("telegram_allow_unconfigured_admins", False))
    if use_advanced:
        raw_chat_ids = str(
            ui_text(
                "Telegram admin chat IDs (comma-separated)",
                default=",".join(recommended_chats),
            )
            or ""
        ).strip()
        raw_user_ids = str(
            ui_text(
                "Telegram admin user IDs (comma-separated)",
                default=",".join(recommended_users),
            )
            or ""
        ).strip()
        admin_chat_ids = [item.strip() for item in raw_chat_ids.split(",") if item.strip()]
        admin_user_ids = [item.strip() for item in raw_user_ids.split(",") if item.strip()]
        allow_unconfigured_admins = ui_confirm(
            "Allow Telegram admins without an explicit allowlist entry?",
            bool(allow_unconfigured_admins),
        )

    updates = {
        "telegram_alert_chat_id": alert_chat_id,
        "telegram_admin_chat_ids": admin_chat_ids,
        "telegram_admin_user_ids": admin_user_ids,
        "telegram_allow_unconfigured_admins": allow_unconfigured_admins,
    }
    guard.save_proactive_security_settings(updates)
    try:
        cfg.set_key("TELEGRAM_ALERT_CHAT_ID", alert_chat_id)
    except Exception:
        pass
    return updates
