#!/usr/bin/env python3
"""[ZW-CLI-2026] Read-only диагностический CLI недвижки (zemlya-bot + zemlya-web).

Обёртка над уже существующим кодом/БД/логами — ничего не дублирует, ничего не мутирует.
Запуск на mac mini из каталога бота:  ./venv/bin/python tools/zemlya_cli.py <cmd> [args]

Команды:
  health              состояния всех сервисов недвижки + HTTP-хелсчеки
  llm                 живой пинг LLM-каскада (gemini/gpt слоты)
  sources [--min N]   Avito/CIAN/Yandex: curl-OK / 403 / fallback за N мин + статус NSPD-sidecar
  nspd <КН>           прямой запрос NSPD-sidecar (детект BlockedIP)
  captcha [--min N]   rosreestr_lk: SUCCESS / timeout / grace / solver
  stats               агрегаты day/week/all (заходы/проверки/оплаты) — raw SQL
  db                  счётчики users/reports/payments
  logs <фильтр> [-n]  греп по логу бота (по умолчанию -n 40)
"""
import os
import re
import sys
import json
import time
import sqlite3
import subprocess
import urllib.request
from datetime import datetime, timedelta

HOME = os.path.expanduser("~")
ROOT = os.path.join(HOME, "Services", "zemlya-bot")
DB = os.path.join(ROOT, "data", "db", "zemlya_bot.db")
LOG = os.path.join(HOME, "Services", "shared", "logs", "zemlya-bot.log")
SIDECAR = "http://127.0.0.1:20155"
BACKEND = "http://127.0.0.1:20140"


def _c(s, code):
    return f"\033[{code}m{s}\033[0m" if sys.stdout.isatty() else s


def ok(s):   return _c(s, "32")
def bad(s):  return _c(s, "31")
def warn(s): return _c(s, "33")
def dim(s):  return _c(s, "90")


def _http(url, timeout=8):
    try:
        with urllib.request.urlopen(url, timeout=timeout) as r:
            return r.status, r.read().decode("utf-8", "replace")
    except Exception as e:
        return None, str(e)


def _db():
    return sqlite3.connect(f"file:{DB}?mode=ro", uri=True, timeout=10)


# ------------------------------------------------------------------ health
def cmd_health(args):
    labels = subprocess.run(["launchctl", "list"], capture_output=True, text=True).stdout
    print("Сервисы недвижки:")
    for line in labels.splitlines():
        parts = line.split("\t")
        if len(parts) < 3:
            continue
        pid, ex, label = parts[0], parts[1], parts[2]
        if not (("zemlya" in label) or ("nspd-fast" in label)):
            continue
        state = ok("● up  ") if pid.strip().isdigit() else (bad("○ down") if ex not in ("0", "-") else dim("· idle"))
        print(f"  {state} {label:45} pid={pid:>7} exit={ex}")
    print("\nHTTP:")
    st, _ = _http(f"{BACKEND}/healthz")
    print(f"  backend :20140/healthz -> {ok(st) if st==200 else bad(st)}")
    st, body = _http(f"{SIDECAR}/health")
    print(f"  nspd-fast :20155/health -> {ok('ok') if st==200 else bad(st)}  {dim(body[:60])}")


# ------------------------------------------------------------------ llm
def cmd_llm(args):
    sys.path.insert(0, ROOT)
    os.chdir(ROOT)
    import asyncio
    from bot.services import omniroute_ai as o
    msgs = [{"role": "user", "content": "Ответь одним словом: работает"}]

    async def main():
        for name, fn in [("gemini-slot", o.post_chat_completion_gemini), ("gpt-slot", o.post_chat_completion_gpt)]:
            try:
                r = await fn(msgs, max_tokens=15, timeout_seconds=40)
                if r:
                    c = (r.get("choices", [{}])[0].get("message", {}) or {}).get("content", "")
                    print(f"  {name:11} -> {ok('OK')} {dim(repr(c[:40]))}")
                else:
                    print(f"  {name:11} -> {bad('None (все модели исчерпаны)')}")
            except Exception as e:
                print(f"  {name:11} -> {bad('ERR')} {type(e).__name__}: {str(e)[:60]}")
        try:
            await o.close_omniroute_session()
        except Exception:
            pass
    asyncio.run(main())


# ------------------------------------------------------------- log helpers
_TS = re.compile(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})")


def _lines_in_window(minutes):
    if not os.path.exists(LOG):
        return []
    cutoff = datetime.now() - timedelta(minutes=minutes) if minutes else None
    out = []
    with open(LOG, encoding="utf-8", errors="replace") as f:
        for ln in f:
            if cutoff:
                m = _TS.match(ln)
                if m:
                    try:
                        if datetime.strptime(m.group(1), "%Y-%m-%d %H:%M:%S") < cutoff:
                            continue
                    except ValueError:
                        pass
            out.append(ln)
    return out


def _count(lines, needle):
    return sum(1 for ln in lines if needle in ln)


# ------------------------------------------------------------------ sources
def cmd_sources(args):
    minutes = _opt_int(args, "--min", 30)
    lines = _lines_in_window(minutes)
    print(f"Рыночные источники за {minutes} мин:")
    for src, okmark in [("avito", "получена через curl_cffi"), ("cian", "cian"), ("yandex", "yandex")]:
        sub = [ln for ln in lines if src in ln.lower()]
        c_ok = _count(sub, "получена через curl") or _count(sub, "curl ok")
        c_403 = _count(sub, "403")
        c_fb = _count(sub, "переключаюсь на Playwright") or _count(sub, "фолбэк")
        tag = ok("ok") if c_403 == 0 else (warn("троттл") if c_fb < 20 else bad("деград"))
        print(f"  {src:8} curl_ok={c_ok:<4} 403={c_403:<4} fallback={c_fb:<4} [{tag}]")
    st, body = _http(f"{SIDECAR}/landplot?cn=50:27:0020446:44", timeout=10)
    blocked = "BlockedIP" in (body or "")
    tag = bad("BlockedIP (бан WAF)") if blocked else (ok("ok") if body and '"ok":true' in body else warn(body[:40]))
    print(f"  NSPD-sidecar -> {tag}")


# ------------------------------------------------------------------ nspd
def cmd_nspd(args):
    if not args:
        print(bad("нужен кадастровый номер: zemlya nspd 50:27:0020446:44"))
        return
    kn = args[0]
    st, body = _http(f"{SIDECAR}/landplot?cn={kn}", timeout=15)
    if st != 200:
        print(bad(f"HTTP {st}: {body[:80]}"))
        return
    try:
        d = json.loads(body)
    except Exception:
        print(warn(body[:120]))
        return
    if d.get("ok"):
        res = d.get("result", {})
        print(ok(f"OK {kn}"), "| geom:", bool(res.get("geometry")), "| keys:", list(res.keys())[:8])
    else:
        reason = d.get("reason", "?")
        print(bad(f"НЕ ok: {reason}") + (dim("  ← IP забанен WAF НСПД") if "Blocked" in str(reason) else ""))


# ------------------------------------------------------------------ captcha
def cmd_captcha(args):
    minutes = _opt_int(args, "--min", 0)
    lines = _lines_in_window(minutes) if minutes else _lines_in_window(0)
    lk = [ln for ln in lines if "rosreestr_lk" in ln or "не успел в grace" in ln]
    win = f"за {minutes} мин" if minutes else "за весь лог"
    print(f"rosreestr_lk {win}:")
    print(f"  SUCCESS:        {ok(_count(lk, 'rosreestr_lk: SUCCESS'))}")
    print(f"  timed out 30s:  {warn(_count(lk, 'timed out'))}")
    print(f"  не успел grace: {warn(_count(lk, 'не успел в grace'))}")
    print(f"  Mistral-PRIMARY:{_count(lk, 'captcha Mistral-PRIMARY')}")
    print(f"  NIM best-vote:  {_count(lk, 'captcha best-vote')}")
    print(f"  Mistral 404:    {bad(_count(lk, 'OmniRoute fallback HTTP 404'))}")


# ------------------------------------------------------------------ stats
def _agg(con, since):
    where_r = f"WHERE created_at >= '{since}'" if since else ""
    where_u = f"WHERE created_at >= '{since}'" if since else ""
    where_p = f"AND completed_at >= '{since}'" if since else ""
    active = con.execute(f"SELECT COUNT(DISTINCT user_id) FROM reports {where_r}").fetchone()[0]
    checks = con.execute(f"SELECT report_mode, COUNT(*) FROM reports {where_r} GROUP BY report_mode").fetchall()
    newu = con.execute(f"SELECT COUNT(*) FROM users {where_u}").fetchone()[0]
    pay = con.execute(f"SELECT COUNT(*), COALESCE(SUM(amount),0) FROM payments WHERE status='completed' AND COALESCE(is_test,0)=0 {where_p}").fetchone()
    return active, dict(checks), newu, pay[0], pay[1] or 0


def cmd_stats(args):
    now = datetime.now()
    con = _db()
    try:
        for label, since in [("Сегодня", now.strftime("%Y-%m-%d 00:00:00")),
                             ("7 дней", (now - timedelta(days=7)).strftime("%Y-%m-%d %H:%M:%S")),
                             ("Всё время", None)]:
            a, ch, nu, pc, ps = _agg(con, since)
            total = sum(ch.values())
            print(f"{label}:")
            print(f"  заходили(проверки): {a}   новые: {nu}   проверок: {total} "
                  f"[free={ch.get('free',0)} pro={ch.get('pro',0)} ai={ch.get('ai',0)}]")
            print(f"  оплат: {pc}   выручка: {ps/100:.0f} ₽")
    finally:
        con.close()


# ------------------------------------------------------------------ db
def cmd_db(args):
    con = _db()
    try:
        u = con.execute("SELECT COUNT(*) FROM users").fetchone()[0]
        r = con.execute("SELECT COUNT(*) FROM reports").fetchone()[0]
        rm = dict(con.execute("SELECT report_mode, COUNT(*) FROM reports GROUP BY report_mode").fetchall())
        pc = con.execute("SELECT COUNT(*) FROM payments WHERE status='completed'").fetchone()[0]
        rev = con.execute("SELECT COALESCE(SUM(amount),0) FROM payments WHERE status='completed' AND COALESCE(is_test,0)=0").fetchone()[0]
        print(f"users:    {u}")
        print(f"reports:  {r}  [free={rm.get('free',0)} pro={rm.get('pro',0)} ai={rm.get('ai',0)}]")
        print(f"payments: {pc} completed,  выручка(боевая): {rev/100:.0f} ₽")
    finally:
        con.close()


# ------------------------------------------------------------------ logs
def cmd_logs(args):
    if not args:
        print(bad("нужен фильтр: zemlya logs avito"))
        return
    flt = args[0]
    n = _opt_int(args, "-n", 40)
    lines = [ln.rstrip() for ln in _lines_in_window(0) if flt.lower() in ln.lower()]
    for ln in lines[-n:]:
        print(ln)
    print(dim(f"— {len(lines)} совпадений, показаны последние {min(n,len(lines))}"))


def _opt_int(args, flag, default):
    if flag in args:
        try:
            return int(args[args.index(flag) + 1])
        except (IndexError, ValueError):
            return default
    return default


CMDS = {
    "health": cmd_health, "llm": cmd_llm, "sources": cmd_sources,
    "nspd": cmd_nspd, "captcha": cmd_captcha, "stats": cmd_stats,
    "db": cmd_db, "logs": cmd_logs,
}


def main():
    if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"):
        print(__doc__)
        return
    cmd = sys.argv[1]
    fn = CMDS.get(cmd)
    if not fn:
        print(bad(f"неизвестная команда: {cmd}"))
        print("доступно:", ", ".join(CMDS))
        sys.exit(2)
    fn(sys.argv[2:])


if __name__ == "__main__":
    main()
