# -*- coding: utf-8 -*-
"""Pre-flight check: is this PC ready to raise the CPC TUN tunnel?"""
from __future__ import annotations

import json
import os
import subprocess
from pathlib import Path
from typing import Any

from net_monitor import load_last
from status import SQUID_DOWN, SQUID_OK, squid_state

CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
PWSH = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "WindowsPowerShell" / "v1.0" / "powershell.exe"
HOME = Path(os.environ["USERPROFILE"])
PROXY = Path(os.environ.get("CLAUDE_PROXY_HOME") or (HOME / ".claude-proxy-control"))
MERGED = PROXY / "merged"

def _scan_system_v2() -> dict[str, Any]:
    """Reliable scan via a temp .ps1 (avoids quoting hell)."""
    script = r'''
$ErrorActionPreference = "Continue"
$homePath = $env:CLAUDE_PROXY_HOME
if (-not $homePath) { $homePath = Join-Path $env:USERPROFILE ".claude-proxy-control" }
$result = @{
  adapters = New-Object System.Collections.ArrayList
  vpnConnections = New-Object System.Collections.ArrayList
  conflictProcesses = New-Object System.Collections.ArrayList
  singBox = New-Object System.Collections.ArrayList
  wintunDevices = New-Object System.Collections.ArrayList
  sotaTemp = New-Object System.Collections.ArrayList
  sotaProcess = $false
  sotaConnectActive = $false
}
$adapterRe = 'Wintun|sing-tun|TAP-Windows|TAP-Win32|OpenVPN|WireGuard|NordLynx|CloudflareWARP|WARP|Tailscale|ZeroTier|Hamachi|SoftEther|Cisco AnyConnect|Fortinet|GlobalProtect|Outline|Amnezia|VPN'
$procNames = @(
  'clash','clash-meta','mihomo','v2ray','xray','wireguard','openvpn','nordvpn','expressvpn',
  'surfshark','mullvad','protonvpn','outline','hysteria','tun2socks','softether','hamachi',
  'zerotier','tailscale','warp-svc','cloudflared','psiphon','amnezia','nekoray','hiddify','sing-box'
)

Get-NetAdapter -ErrorAction SilentlyContinue | ForEach-Object {
  $name = [string]$_.Name
  $desc = [string]$_.InterfaceDescription
  $status = [string]$_.Status
  $isCpc = ($name -like 'cpc*')
  $looks = ($name -match $adapterRe) -or ($desc -match $adapterRe)
  if ($isCpc -or $looks) {
    [void]$result.adapters.Add(@{
      name = $name; description = $desc; status = $status
      isCpc = [bool]$isCpc; looksTunnel = [bool]$looks
    })
  }
}

try {
  Get-VpnConnection -ErrorAction SilentlyContinue | ForEach-Object {
    [void]$result.vpnConnections.Add(@{
      name = [string]$_.Name
      connectionStatus = [string]$_.ConnectionStatus
      tunnelType = [string]$_.TunnelType
    })
  }
} catch {}

# Parent map for sing-box attribution (Sota vs Amnezia vs CPC)
$parentMap = @{}
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | ForEach-Object {
  $parentMap[[int]$_.ProcessId] = $_
}

Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | ForEach-Object {
  $n = ([string]$_.Name).ToLowerInvariant()
  $cmd = [string]$_.CommandLine
  $path = [string]$_.ExecutablePath
  $base = [IO.Path]::GetFileNameWithoutExtension($n)

  if ($n -match 'sota' -or ($path -and $path -match '(?i)SotaConnect') -or ($cmd -and $cmd -match '(?i)SotaConnect|SotaVPN')) {
    $result.sotaProcess = $true
  }

  $hit = $false
  foreach ($pat in $procNames) {
    if ($base -like ("*" + $pat + "*") -or $n -like ("*" + $pat + "*")) { $hit = $true; break }
    if ($path -and $path.ToLowerInvariant().Contains($pat)) { $hit = $true; break }
  }
  if (-not $hit) { return }

  $isCpc = $false
  $isSota = $false
  $parentName = ""
  if ($n -eq 'sing-box.exe') {
    $pp = $parentMap[[int]$_.ParentProcessId]
    if ($pp) { $parentName = [string]$pp.Name }
    if ($path -and $path.StartsWith($homePath, [StringComparison]::OrdinalIgnoreCase)) { $isCpc = $true }
    if ($cmd -and ($cmd.IndexOf($homePath, [StringComparison]::OrdinalIgnoreCase) -ge 0 -or $cmd -match 'claude-proxy-control')) { $isCpc = $true }
    if ($parentName -match '(?i)sota') { $isSota = $true }
    if ($path -and $path -match '(?i)SotaConnect') { $isSota = $true }
    if ($isSota) { $result.sotaConnectActive = $true }
    [void]$result.singBox.Add(@{
      pid = [int]$_.ProcessId; path = $path; isCpc = $isCpc; isSota = $isSota; parent = $parentName
    })
    if ($isCpc -or $isSota) { return }
  }

  # Amnezia service often runs idle — mark soft
  $soft = $false
  if ($n -match 'amnezia') { $soft = $true }

  [void]$result.conflictProcesses.Add(@{
    pid = [int]$_.ProcessId
    name = [string]$_.Name
    path = $path
    soft = $soft
  })
}

# If Sota's sing-box is up and a non-cpc sing-tun adapter is Up → Connect active
$sotaSb = @($result.singBox | Where-Object { $_.isSota })
if ($sotaSb.Count -gt 0) {
  $result.sotaConnectActive = $true
  foreach ($a in @($result.adapters)) {
    if (-not $a.isCpc -and $a.status -eq 'Up' -and ($a.description -match 'sing-tun' -or $a.name -match '^tun')) {
      $a.isSota = $true
    }
  }
}

try {
  Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object {
    $_.InstanceId -match 'WINTUN' -or $_.FriendlyName -match 'sing-tun|Wintun|vpn-claude|claude-tun'
  } | ForEach-Object {
    [void]$result.wintunDevices.Add(@{
      friendlyName = [string]$_.FriendlyName
      status = [string]$_.Status
      instanceId = [string]$_.InstanceId
    })
  }
} catch {}

$temps = @('C:\Windows\TEMP','C:\Windows\Temp',$env:TEMP) | Select-Object -Unique
foreach ($t in $temps) {
  if (-not (Test-Path -LiteralPath $t)) { continue }
  Get-ChildItem -LiteralPath $t -Filter 'sota-singbox-*.json' -Force -ErrorAction SilentlyContinue |
    Where-Object { $_.LastWriteTime -gt (Get-Date).AddSeconds(-45) } |
    ForEach-Object {
      [void]$result.sotaTemp.Add(@{
        path = $_.FullName
        ageSec = [int]((Get-Date) - $_.LastWriteTime).TotalSeconds
      })
    }
}

$result | ConvertTo-Json -Depth 6 -Compress
'''
    tmp = PROXY / "merged" / "_tun_ready_scan.ps1"
    try:
        tmp.parent.mkdir(parents=True, exist_ok=True)
        tmp.write_text(script, encoding="utf-8")
    except Exception as e:
        return {"error": f"write script: {e}"}

    env = os.environ.copy()
    env["CLAUDE_PROXY_HOME"] = str(PROXY)
    try:
        p = subprocess.run(
            [
                str(PWSH),
                "-NoProfile",
                "-ExecutionPolicy",
                "Bypass",
                "-File",
                str(tmp),
            ],
            capture_output=True,
            text=True,
            timeout=55,
            creationflags=CREATE_NO_WINDOW,
            env=env,
        )
        raw = (p.stdout or "").strip()
        if not raw:
            return {"error": (p.stderr or "empty stdout")[-400:]}
        start = raw.find("{")
        end = raw.rfind("}")
        if start < 0 or end <= start:
            return {"error": "no json", "raw": raw[-400:]}
        return json.loads(raw[start : end + 1])
    except Exception as e:
        return {"error": str(e)}


def _as_list(val: Any) -> list:
    if val is None:
        return []
    if isinstance(val, list):
        return val
    if isinstance(val, dict):
        return [val]
    return []


def run_tun_ready_check(
    *,
    license_active: bool,
    proxy_ready: bool,
    proxy_host: str,
    watcher_ok: bool,
    nodes: int,
    our_tunnel_up: bool,
) -> dict[str, Any]:
    """
    Full readiness report.
    Returns {ready, summary, lines:[{level,text}], blockers, warnings, explain}.
    level: ok | warn | bad | info
    """
    scan = _scan_system_v2()
    lines: list[dict[str, str]] = []
    blockers: list[str] = []
    warnings: list[str] = []

    lines.append(
        {
            "level": "info",
            "text": (
                "TUN — это виртуальная сетевая карта (драйвер Wintun/sing-tun). "
                "Программа поднимает её через sing-box: весь трафик Claude идёт "
                "в наш туннель → Squid → интернет. Обычный Wi‑Fi/Ethernet при этом остаётся."
            ),
        }
    )

    if scan.get("error"):
        lines.append({"level": "warn", "text": f"Скан системы частично недоступен: {scan.get('error')}"})
        warnings.append("scan_error")

    # Prerequisites
    if license_active:
        lines.append({"level": "ok", "text": "Лицензия активна"})
    else:
        lines.append({"level": "bad", "text": "Лицензия не активна — туннель для Claude не запускаем"})
        blockers.append("license")

    # Два разных вопроса, которые раньше сливались в одну строку «Squid-прокси
    # с сервера готов». Пользователь читал её как «прокси работает», а внизу
    # главного окна в ту же секунду висело «прокси-сервер не отвечает».
    if proxy_ready:
        lines.append(
            {"level": "ok", "text": f"Реквизиты прокси получены с сервера ({proxy_host or '?'})"}
        )
    else:
        lines.append({"level": "bad", "text": "Нет прокси с сервера — активируйте ключ"})
        blockers.append("proxy")

    # Живой вердикт — тот же самый, что показывает главное окно (status.py).
    if proxy_ready:
        state, detail = squid_state(MERGED, load_last() or {})
        if state == SQUID_OK:
            lines.append({"level": "ok", "text": "Прокси отвечает — Claude через него пойдёт"})
        elif state == SQUID_DOWN:
            lines.append(
                {
                    "level": "warn",
                    "text": (
                        f"Прокси не отвечает: {detail}. Туннель поднимется, остальные "
                        "каналы будут работать, но Claude через него не пойдёт."
                    ),
                }
            )
            warnings.append("squid")
        else:
            lines.append({"level": "info", "text": "Прокси ещё не проверен — идёт первая проверка"})

    # Sota даёт только канал VPN. Без неё программа обязана работать: Claude идёт
    # через Squid, что-то напрямую, незнакомое закрыто. Поэтому отсутствие узлов
    # и ловца — предупреждение, а не запрет на запуск.
    if watcher_ok:
        lines.append({"level": "ok", "text": "Ловец конфигов VPN работает"})
    else:
        lines.append({"level": "warn", "text": "Ловец конфигов VPN не работает — канал VPN недоступен"})
        warnings.append("watcher")

    if nodes > 0:
        lines.append({"level": "ok", "text": f"Узлы VPN в базе: {nodes}"})
    else:
        lines.append(
            {
                "level": "warn",
                "text": (
                    "Узлов VPN нет — канал VPN недоступен, приложения на нём будут "
                    "заблокированы. Остальное работает. Чтобы включить VPN, сделайте "
                    "Connect→Disconnect в Sota."
                ),
            }
        )
        warnings.append("nodes")

    singbox_bin: Path | None = None
    candidates: list[Path] = []
    try:
        roots = [PROXY / "sing-box", PROXY]
        for root in roots:
            if not root.is_dir():
                continue
            for d in sorted(root.glob("sing-box-*-windows-amd64"), reverse=True):
                candidates.append(d / "sing-box.exe")
    except Exception:
        pass
    candidates.extend(
        [
            PROXY / "bin" / "sing-box.exe",
            PROXY / "sing-box.exe",
        ]
    )
    for cand in candidates:
        if cand.is_file():
            singbox_bin = cand
            break
    if singbox_bin:
        lines.append({"level": "ok", "text": f"sing-box найден: {singbox_bin.parent.name}\\{singbox_bin.name}"})
    else:
        if (PROXY / "Start-VpnClaude.ps1").is_file():
            lines.append(
                {
                    "level": "warn",
                    "text": "sing-box.exe пока не найден — при первом старте скрипт должен его подготовить",
                }
            )
            warnings.append("singbox_bin")
        else:
            lines.append({"level": "bad", "text": "Runtime не установлен (нет Start-VpnClaude.ps1)"})
            blockers.append("runtime")

    if our_tunnel_up:
        lines.append(
            {
                "level": "ok",
                "text": "Наш TUN уже поднят (cpc*) — ПК сейчас в режиме Claude+VPN",
            }
        )
    else:
        lines.append({"level": "ok", "text": "Наш TUN сейчас выключен — можно поднимать"})

    # Adapters: CPC / Sota Connect / other VPN
    foreign_up = []
    sota_tun_up = []
    for a in _as_list(scan.get("adapters")):
        name = a.get("name") or "?"
        status = (a.get("status") or "").lower()
        if a.get("isCpc"):
            continue
        if status != "up" or not a.get("looksTunnel"):
            continue
        label = f"{name} ({a.get('description') or '?'})"
        if a.get("isSota") or scan.get("sotaConnectActive"):
            # sing-tun while Sota's sing-box is running = Sota Connect
            desc = (a.get("description") or "").lower()
            if "sing-tun" in desc or str(name).lower().startswith("tun"):
                sota_tun_up.append(label)
                continue
        foreign_up.append(label)

    # If Sota sing-box active but adapter wasn't tagged isSota (JSON quirk), still count sing-tun
    if scan.get("sotaConnectActive") and not sota_tun_up:
        for a in _as_list(scan.get("adapters")):
            status = (a.get("status") or "").lower()
            desc = (a.get("description") or "").lower()
            name = a.get("name") or "?"
            if a.get("isCpc") or status != "up":
                continue
            if "sing-tun" in desc or str(name).lower().startswith("tun"):
                sota_tun_up.append(f"{name} ({a.get('description') or '?'})")

    sota_connect = bool(scan.get("sotaConnectActive") or sota_tun_up or _as_list(scan.get("sotaTemp")))
    if sota_connect and not our_tunnel_up:
        detail = "; ".join(sota_tun_up[:4]) if sota_tun_up else "sing-box от sota-daemon"
        lines.append(
            {
                "level": "bad",
                "text": (
                    f"Sota Connect сейчас ВКЛЮЧЕН ({detail}). "
                    "Сделайте Disconnect перед стартом Claude+VPN — иначе два туннеля будут драться."
                ),
            }
        )
        blockers.append("sota_connect")
    elif scan.get("sotaProcess"):
        lines.append(
            {
                "level": "ok",
                "text": "Sota запущена, Connect выключен — так и нужно перед стартом нашего TUN",
            }
        )
    else:
        lines.append({"level": "info", "text": "Sota не обнаружена как процесс (для ловца узлов она нужна)"})

    if foreign_up:
        msg = "Другие VPN/TUN-адаптеры ВКЛЮЧЕНЫ: " + "; ".join(foreign_up[:6])
        lines.append({"level": "bad", "text": msg + " — выключите перед стартом"})
        blockers.append("foreign_tun")
    else:
        lines.append({"level": "ok", "text": "Чужих активных VPN/TUN-адаптеров (кроме Sota выше) не видно"})

    # Windows VPN profiles connected
    win_vpn = [
        v
        for v in _as_list(scan.get("vpnConnections"))
        if str(v.get("connectionStatus") or "").lower() in {"connected", "подключено"}
    ]
    if win_vpn:
        names = ", ".join(v.get("name") or "?" for v in win_vpn[:5])
        lines.append({"level": "bad", "text": f"Встроенный VPN Windows подключён: {names}"})
        blockers.append("win_vpn")
    else:
        lines.append({"level": "ok", "text": "Встроенный VPN Windows не подключён"})

    # Conflict processes — Amnezia idle service = warn; active tunnel softwares = bad
    conflicts = _as_list(scan.get("conflictProcesses"))
    hard_conflicts = []
    soft_conflicts = []
    for c in conflicts:
        name = (c.get("name") or "").lower()
        if "sing-box" in name or "sota" in name:
            continue
        label = f"{c.get('name')} (pid {c.get('pid')})"
        if c.get("soft") or "amnezia" in name:
            soft_conflicts.append(label)
        else:
            hard_conflicts.append(label)

    hard_conflicts = list(dict.fromkeys(hard_conflicts))
    soft_conflicts = list(dict.fromkeys(soft_conflicts))
    if hard_conflicts:
        lines.append(
            {
                "level": "bad",
                "text": "Мешающий софт запущен: " + "; ".join(hard_conflicts[:8])
                + " — закройте перед поднятием TUN",
            }
        )
        blockers.append("conflict_soft")
    else:
        lines.append({"level": "ok", "text": "Активных конфликтующих VPN-клиентов (Clash/WireGuard/…) не видно"})

    if soft_conflicts:
        lines.append(
            {
                "level": "warn",
                "text": (
                    "Фоновые VPN-службы (могут мешать): " + "; ".join(soft_conflicts[:6])
                    + " — если свой TUN не нужен, лучше остановить/удалить"
                ),
            }
        )
        warnings.append("idle_vpn_service")

    # Other sing-box (not CPC, not Sota)
    other_sb = [
        s
        for s in _as_list(scan.get("singBox"))
        if not s.get("isCpc") and not s.get("isSota")
    ]
    our_sb = [s for s in _as_list(scan.get("singBox")) if s.get("isCpc")]
    if other_sb:
        lines.append(
            {
                "level": "warn",
                "text": f"Чужой sing-box запущен (x{len(other_sb)}) — не Sota и не наш CPC",
            }
        )
        warnings.append("other_singbox")
    if our_sb and not our_tunnel_up:
        lines.append(
            {
                "level": "warn",
                "text": "Процесс CPC sing-box есть, но TUN не Up — возможен зомби; Stop Claude или Диагностика",
            }
        )
        warnings.append("zombie_singbox")

    wintun = _as_list(scan.get("wintunDevices"))
    orphan = [d for d in wintun if str(d.get("status") or "").lower() not in {"ok", "started"}]
    if orphan:
        lines.append(
            {
                "level": "warn",
                "text": (
                    f"Есть проблемные Wintun-устройства ({len(orphan)}) — "
                    "при старте скрипт попробует их почистить"
                ),
            }
        )
        warnings.append("orphan_wintun")
    elif wintun:
        lines.append({"level": "info", "text": f"Wintun-устройств в системе: {len(wintun)} (ок)"})

    ready = len(blockers) == 0
    if our_tunnel_up and ready:
        summary = "TUN уже работает. Конфликтов, мешающих текущему туннелю, не видно."
    elif ready:
        summary = "ПК готов к поднятию TUN. Можно жать «Запустить Claude»."
    else:
        summary = "ПК пока НЕ готов к TUN — сначала уберите блокеры ниже."

    return {
        "ready": ready,
        "summary": summary,
        "lines": lines,
        "blockers": blockers,
        "warnings": warnings,
        "scan": scan,
        "explain": (
            "TUN (Tunnel) — виртуальная сетевая карта. "
            "Мы создаём адаптер вида cpcXXXX через Wintun/sing-box и направляем трафик Claude в VPN-узел Sota, "
            "а оттуда на Squid-прокси лицензии."
        ),
    }


def format_report_text(report: dict[str, Any]) -> str:
    parts = [report.get("summary") or "", ""]
    for row in report.get("lines") or []:
        lvl = row.get("level") or "info"
        mark = {"ok": "✅", "bad": "❌", "warn": "⚠️", "info": "ℹ️"}.get(lvl, "•")
        parts.append(f"{mark}  {row.get('text')}")
    return "\n".join(parts).strip()
