# -*- coding: utf-8 -*-
"""Одно место, где решается, что сейчас с программой.

На один и тот же вопрос отвечали три независимых куска кода, и отвечали
по-разному. Живой пример с чистой машины: отчёт «Готовность ПК к TUN» писал
«Squid-прокси с сервера готов (91.124.19.37)», а внизу главного окна в ту же
секунду висело «прокси-сервер не отвечает — Claude через него не пойдёт».
Оба были по-своему правы: первый проверял, что реквизиты прокси пришли с
сервера, второй — что прокси отвечает. Разные вопросы, похожие слова.

Теперь вопросы называются по-разному и считаются здесь:

* `squid_state`  — прокси отвечает прямо сейчас?
* `vpn_state`    — есть ли канал VPN (узлы Sota)?
* `channel_note` — что из этого сейчас недоступно, одной строкой.

Sota здесь принципиально не блокирует ничего: она даёт один канал из четырёх.
"""
from __future__ import annotations

import base64
import json
import os
import socket
import time
from pathlib import Path
from typing import Any

# Отчёт монитора живёт своей жизнью: он приходит из планировщика раз в пять
# минут. Пока он свежий — верим ему, дальше идём и проверяем сами.
MONITOR_FRESH_SEC = 180
# Свой замер держим ровно столько, чтобы кнопка не мигала между обновлениями UI.
LIVE_FRESH_SEC = 45

SQUID_OK = "ok"
SQUID_DOWN = "down"
SQUID_UNKNOWN = "unknown"

VPN_OK = "ok"
VPN_NO_NODES = "no_nodes"


def squid_alive(
    host: str, port: int, user: str = "", password: str = "", timeout: float = 5.0
) -> tuple[bool, str]:
    """Настоящий обмен с прокси. (живой, пояснение).

    Проверять сокетом нельзя: внутри поднятого TUN `connect()` успешен к любому
    адресу — sing-box завершает рукопожатие локально, ещё не зная, достижим ли
    тот конец. Проверено на TEST-NET: 203.0.113.9 «отвечал» так же бодро, как
    настоящий прокси. Поэтому шлём запрос и ждём строку статуса HTTP.
    """
    if not host or not port:
        return False, "реквизиты прокси не заданы"
    target = "api.anthropic.com:443"
    req = f"CONNECT {target} HTTP/1.1\r\nHost: {target}\r\n"
    if user:
        cred = base64.b64encode(f"{user}:{password}".encode()).decode()
        req += f"Proxy-Authorization: Basic {cred}\r\n"
    req += "\r\n"
    sock = None
    try:
        sock = socket.create_connection((host, int(port)), timeout=timeout)
        sock.settimeout(timeout)
        sock.sendall(req.encode())
        head = sock.recv(256).decode("latin-1", "replace")
        if not head.startswith("HTTP/"):
            return False, "на том конце не прокси"
        # Тексты человеческие: они попадают прямо на главный экран. Код ответа
        # пользователю ничего не говорит — «прокси ответил 503» он прочтёт как
        # поломку своего компьютера.
        code = head.split(" ")[1] if " " in head else "?"
        if code == "200":
            return True, "связь есть"
        if code == "407":
            return False, "сервер не принял ваш ключ доступа"
        if code == "403":
            return False, "сервер отказал в доступе"
        return False, "сервер отвечает с ошибкой"
    except socket.timeout:
        return False, "сервер не отвечает"
    except OSError:
        return False, "до сервера не достучаться"
    except Exception as e:  # проверка не должна ронять вызывающего
        return False, f"проверка не удалась ({e})"
    finally:
        if sock is not None:
            try:
                sock.close()
            except Exception:
                pass


def _live_path(merged: Path) -> Path:
    return merged / "squid-live.json"


def save_live_squid(merged: Path, ok: bool, detail: str) -> None:
    """Запомнить свой замер, чтобы отрисовка не ходила в сеть."""
    try:
        merged.mkdir(parents=True, exist_ok=True)
        _live_path(merged).write_text(
            json.dumps({"ok": bool(ok), "detail": detail, "ts": time.time()}),
            encoding="utf-8",
        )
    except Exception:
        pass


def _load_live_squid(merged: Path) -> dict[str, Any] | None:
    try:
        data = json.loads(_live_path(merged).read_text(encoding="utf-8"))
    except Exception:
        return None
    if not isinstance(data, dict) or "ok" not in data:
        return None
    if time.time() - float(data.get("ts") or 0) > LIVE_FRESH_SEC:
        return None
    return data


def _monitor_age(sample: dict[str, Any] | None) -> float:
    """Сколько секунд назад монитор записал этот отчёт."""
    if not sample:
        return 1e9
    stamp = str(sample.get("time") or "")
    for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
        try:
            return max(0.0, time.time() - time.mktime(time.strptime(stamp, fmt)))
        except Exception:
            continue
    return 1e9


def squid_state(merged: Path, monitor: dict[str, Any] | None) -> tuple[str, str]:
    """Отвечает ли прокси. ("ok"|"down"|"unknown", пояснение).

    Порядок важен. Свой свежий замер главнее отчёта планировщика: монитор
    запускается раз в пять минут, и на чистой машине его первый запуск успевает
    случиться ДО активации ключа. Тогда в отчёте навсегда остаётся
    «proxy.env неполный», и кнопка «Запустить Claude» заперта до следующего
    круга — ровно это и произошло при первом тесте на чужом компьютере.
    """
    live = _load_live_squid(merged)
    if live is not None:
        return (SQUID_OK if live.get("ok") else SQUID_DOWN), str(live.get("detail") or "")

    sq = (monitor or {}).get("squid")
    if isinstance(sq, dict) and "ok" in sq and _monitor_age(monitor) <= MONITOR_FRESH_SEC:
        return (SQUID_OK if sq.get("ok") else SQUID_DOWN), str(sq.get("detail") or "")

    # Ничего свежего нет. «Неизвестно» — не «сломано»: запрещать запуск из-за
    # отсутствия данных значит запирать программу на ровном месте.
    return SQUID_UNKNOWN, "проверка ещё не проходила"


def vpn_state(nodes: int, watcher_ok: bool) -> tuple[str, str]:
    """Есть ли канал VPN. Никогда не блокирует запуск — это один канал из четырёх."""
    if int(nodes or 0) > 0:
        return VPN_OK, ""
    return VPN_NO_NODES, "Канал «VPN» пока недоступен — узел Sota не пойман."


def channel_note(squid: tuple[str, str], vpn: tuple[str, str]) -> str:
    """Одна строка про то, чего сейчас нет. Пусто — значит всё на месте.

    Про непроверенный прокси молчим: «ещё не проверен» — это про нашу кухню,
    а не про то, что пользователю делать.
    """
    bits = []
    if squid[0] == SQUID_DOWN:
        bits.append(f"Claude сейчас не откроется: {squid[1]}.")
    if vpn[0] != VPN_OK and vpn[1]:
        bits.append(vpn[1])
    return " ".join(bits)


def read_proxy_env(home: Path) -> dict[str, str]:
    """Реквизиты прокси из proxy.env. Пустой словарь, если файла нет."""
    out: dict[str, str] = {}
    path = home / "proxy.env"
    try:
        for line in path.read_text(encoding="utf-8-sig").splitlines():
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, _, value = line.partition("=")
            out[key.strip()] = value.strip()
    except Exception:
        return {}
    return out


def probe_and_save(home: Path) -> tuple[str, str]:
    """Сходить к прокси и запомнить ответ. Вызывать только из фонового потока."""
    env = read_proxy_env(home)
    host = env.get("PROXY_HOST", "")
    port = int(env.get("PROXY_PORT") or 0) if str(env.get("PROXY_PORT") or "").isdigit() else 0
    if not host or not port:
        return SQUID_UNKNOWN, "реквизиты прокси ещё не получены"
    ok, detail = squid_alive(host, port, env.get("PROXY_USER", ""), env.get("PROXY_PASS", ""))
    save_live_squid(home / "merged", ok, detail)
    return (SQUID_OK if ok else SQUID_DOWN), detail


def home_dir() -> Path:
    return Path(
        os.environ.get("CLAUDE_PROXY_HOME")
        or (Path(os.environ["USERPROFILE"]) / ".claude-proxy-control")
    )
