import os
import time
import socket
import subprocess
import json
import threading
import hashlib
from urllib.request import urlopen, Request
from urllib.parse import urlencode
from urllib.error import URLError, HTTPError

from flask import Flask, jsonify, render_template, send_from_directory, send_file, request, Response

try:
    from flask_sock import Sock
except ImportError:
    Sock = None

try:
    from websocket import create_connection as _ws_create_connection
    from websocket import WebSocketConnectionClosedException as _WsClosed
except ImportError:
    _ws_create_connection = None
    _WsClosed = Exception

from state import get_cpu_usage_percent
from network_manager import (
    load_network_config, save_network_config, get_current_network_info,
    switch_network_mode, network_config_lock, NETWORK_DEFAULTS,
)
from terminal import terminal_session
from transponder import (
    load_transponder_config,
    normalize_transponder_config,
    save_transponder_config,
    merge_transponder_request,
    build_preview,
    send_transmission,
    build_slot_udp_payload,
    is_aistx_phy_available,
    parse_nrzi_hex,
    send_raw_nrzi_packet,
    tx_gpio_pulse,
)

app = Flask(__name__)
sock = Sock(app) if Sock else None


# ==================== ais_hub upstream ====================

AIS_HUB_URL = os.environ.get("AIS_HUB_URL", "http://127.0.0.1:8081").rstrip("/")


def _ais_hub_ws_url():
    base = AIS_HUB_URL
    if base.startswith("https://"):
        return "wss://" + base[len("https://"):] + "/ws"
    if base.startswith("http://"):
        return "ws://" + base[len("http://"):] + "/ws"
    return "ws://" + base + "/ws"


# ==================== helpers ====================

def _append_client_log_line(line: str):
    """Append a single line to client_errors.log (best-effort)."""
    try:
        path = os.path.join(os.path.dirname(__file__), "client_errors.log")
        with open(path, "a", encoding="utf-8") as f:
            f.write(line.rstrip("\n") + "\n")
    except Exception:
        pass


# ==================== WebSocket terminal ====================
if sock is not None and os.name == "posix":

    @sock.route("/ws/terminal")
    def ws_terminal(ws):
        terminal_session(ws)


# ==================== WebSocket proxy to ais_hub /ws ====================
if sock is not None:

    @sock.route("/ws")
    def ws_ais_hub_proxy(ws):
        """Прозрачный WS-прокси к ais_hub /ws (localhost:8081)."""
        if _ws_create_connection is None:
            # Не закрываем WS сразу: иначе браузер будет переподключаться в цикле и шуметь в консоли.
            # Держим соединение открытым и периодически напоминаем про отсутствующую зависимость.
            while True:
                try:
                    ws.send(json.dumps({"type": "error", "ts": time.time(),
                                        "data": {"error": "websocket-client not installed on server"}}))
                except Exception:
                    return
                try:
                    time.sleep(5)
                except Exception:
                    return

        upstream_url = _ais_hub_ws_url()
        # Если ais_hub временно недоступен, не рвём соединение с браузером:
        # держим WS открытым и периодически ретраим подключение к upstream.
        upstream = None
        backoff = 1.0
        while upstream is None:
            try:
                upstream = _ws_create_connection(upstream_url, timeout=5)
                break
            except Exception as e:
                try:
                    ws.send(json.dumps({"type": "error", "ts": time.time(),
                                        "data": {"error": f"ais_hub unreachable: {e}"}}))
                except Exception:
                    # Клиент ушёл — дальше ретраить нет смысла.
                    return
                try:
                    time.sleep(backoff)
                except Exception:
                    return
                backoff = min(backoff * 2.0, 10.0)

        stop = threading.Event()

        def upstream_to_client():
            try:
                while not stop.is_set():
                    try:
                        msg = upstream.recv()
                    except _WsClosed:
                        break
                    except Exception:
                        break
                    if msg is None or msg == "":
                        break
                    if isinstance(msg, bytes):
                        try:
                            msg = msg.decode("utf-8", errors="ignore")
                        except Exception:
                            continue
                    try:
                        ws.send(msg)
                    except Exception:
                        break
            finally:
                stop.set()

        t = threading.Thread(target=upstream_to_client, daemon=True)
        t.start()

        try:
            while not stop.is_set():
                try:
                    # flask_sock/simple_websocket have slightly different receive() signatures
                    # across versions; some don't support timeout=, and some raise on timeout.
                    try:
                        data = ws.receive(timeout=30)
                    except TypeError:
                        data = ws.receive()
                except Exception as e:
                    # Don't tear down the whole proxy on a benign receive timeout.
                    if "timeout" in str(e).lower():
                        continue
                    break
                if data is None:
                    break
                try:
                    if isinstance(data, bytes):
                        upstream.send_binary(data)
                    else:
                        upstream.send(data)
                except Exception:
                    break
        finally:
            stop.set()
            try:
                upstream.close()
            except Exception:
                pass


# ==================== Pages ====================

@app.route("/")
def index():
    return render_template("index.html")


@app.route("/cert")
def cert_install_page():
    return render_template("cert.html")


# ==================== REST proxy to ais_hub /api/v1/* ====================

_HOP_HEADERS = {
    "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
    "te", "trailers", "transfer-encoding", "upgrade", "content-encoding",
    "content-length", "host",
}


@app.route("/api/v1/<path:rest>", methods=["GET", "POST", "PUT", "DELETE"])
def proxy_ais_hub(rest):
    """Прозрачный прокси REST /api/v1/* на ais_hub (127.0.0.1:8081)."""
    url = f"{AIS_HUB_URL}/api/v1/{rest}"
    qs = request.query_string.decode("utf-8") if request.query_string else ""
    if qs:
        url = f"{url}?{qs}"

    method = request.method.upper()
    body = None
    if method in ("POST", "PUT"):
        body = request.get_data() or b""

    headers = {}
    ct = request.headers.get("Content-Type")
    if ct:
        headers["Content-Type"] = ct

    req = Request(url, data=body, method=method, headers=headers)
    timeout = 10.0 if method in ("POST", "PUT") else 5.0

    try:
        resp = urlopen(req, timeout=timeout)
        payload = resp.read()
        status = getattr(resp, "status", 200) or 200
        out_headers = {}
        for k, v in resp.headers.items():
            if k.lower() in _HOP_HEADERS:
                continue
            out_headers[k] = v
        out_headers.setdefault("Cache-Control", "no-store")
        return Response(payload, status=status, headers=out_headers)
    except HTTPError as e:
        try:
            payload = e.read()
        except Exception:
            payload = b""
        out_headers = {}
        try:
            for k, v in e.headers.items():
                if k.lower() in _HOP_HEADERS:
                    continue
                out_headers[k] = v
        except Exception:
            pass
        out_headers.setdefault("Cache-Control", "no-store")
        return Response(payload, status=e.code, headers=out_headers)
    except URLError as e:
        return jsonify({"error": f"ais_hub unreachable: {e.reason}"}), 503
    except Exception as e:
        return jsonify({"error": f"proxy error: {e}"}), 502


# ==================== API ====================

@app.route("/api/terminal")
def api_terminal():
    """Доступность веб-консоли (PTY + WebSocket)."""
    return jsonify({"pty": os.name == "posix", "ws": sock is not None})


@app.route("/api/version")
def api_version():
    """Версия/сборка для отладки кэша фронта и деплоя."""
    try:
        here = os.path.dirname(__file__)
        def _mtime(p):
            try:
                return int(os.path.getmtime(os.path.join(here, p)))
            except Exception:
                return None
        return jsonify({
            "server_time": int(time.time()),
            "routes_py_mtime": _mtime("routes.py"),
            "app_js_mtime": _mtime(os.path.join("static", "js", "app.js")),
            "index_html_mtime": _mtime(os.path.join("templates", "index.html")),
        })
    except Exception as e:
        return jsonify({"server_time": int(time.time()), "error": str(e)})


@app.route("/api/sysinfo")
def api_sysinfo():
    """Локальная система: CPU %, температура, память, uptime. ais_hub этого не знает."""
    now = int(time.time())
    out = {"server_now": now}

    try:
        with open("/proc/uptime", "r") as f:
            out["sys_uptime"] = int(float(f.read().split()[0]))
    except Exception:
        out["sys_uptime"] = None

    try:
        with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
            out["cpu_temp"] = round(int(f.read().strip()) / 1000.0, 1)
    except Exception:
        out["cpu_temp"] = None

    out["cpu_percent"] = get_cpu_usage_percent()

    try:
        mi = {}
        with open("/proc/meminfo", "r") as f:
            for line in f:
                k, v = line.split(":", 1)
                mi[k.strip()] = int(v.strip().split()[0])
        total = mi.get("MemTotal", 0)
        avail = mi.get("MemAvailable", mi.get("MemFree", 0))
        out["mem_total_mb"] = round(total / 1024)
        out["mem_used_mb"] = round((total - avail) / 1024)
        out["mem_pct"] = round((total - avail) / total * 100, 1) if total else 0
    except Exception:
        out["mem_total_mb"] = None
        out["mem_used_mb"] = None
        out["mem_pct"] = None

    return jsonify(out)


@app.route("/api/client_log", methods=["POST"])
def api_client_log():
    """
    Приём логов/ошибок из браузера (front-end).
    Пишет в client_errors.log рядом с routes.py.
    """
    try:
        data = request.get_json(force=True, silent=True) or {}
    except Exception:
        data = {}

    level = str(data.get("level") or "info").lower()
    msg = str(data.get("msg") or "")
    ctx = data.get("ctx")
    ts = data.get("ts")
    url = data.get("url")
    ua = data.get("ua")

    try:
        now = time.strftime("%Y-%m-%d %H:%M:%S")
        line = f"[{now}] level={level} msg={msg} url={url} ua={ua} ctx={ctx} ts={ts}"
        _append_client_log_line(line)
    except Exception:
        pass

    return jsonify({"ok": True})


@app.route("/api/client_log_tail")
def api_client_log_tail():
    """Хвост client_errors.log для диагностики без SSH."""
    try:
        n = request.args.get("n", "200")
        try:
            n = int(n)
        except Exception:
            n = 200
        n = max(1, min(2000, n))

        path = os.path.join(os.path.dirname(__file__), "client_errors.log")
        if not os.path.exists(path):
            return jsonify({"ok": True, "exists": False, "lines": []})

        with open(path, "r", encoding="utf-8", errors="replace") as f:
            lines = f.readlines()
        tail = [ln.rstrip("\n") for ln in lines[-n:]]
        return jsonify({"ok": True, "exists": True, "lines": tail})
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


@app.route("/api/transponder", methods=["GET"])
def api_transponder_get():
    """Настройки Class B. ownship тянется фронтом отдельно через /api/v1/ownship."""
    try:
        cfg = normalize_transponder_config(load_transponder_config())
        return jsonify(
            {
                "ok": True,
                "config": cfg,
                "aistx_phy_available": is_aistx_phy_available(),
            }
        )
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


def _fetch_ownship_from_hub():
    """Best-effort: последний GPS-fix из ais_hub. Возвращает dict (возможно пустой)."""
    try:
        resp = urlopen(f"{AIS_HUB_URL}/api/v1/ownship", timeout=2.0)
        raw = resp.read()
        own = json.loads(raw.decode("utf-8", errors="ignore"))
        if not isinstance(own, dict):
            return {}
        # Адаптируем имена полей к тем, что ожидает transponder (course/speed/...).
        return {
            "lat": own.get("lat"),
            "lon": own.get("lon"),
            "course": own.get("cog"),
            "speed": own.get("sog"),
            "heading": None,
            "timestamp": own.get("ts"),
            "satellites": own.get("sats"),
            "fix_quality": own.get("fix_quality"),
        }
    except Exception:
        return {}


@app.route("/api/transponder", methods=["POST"])
def api_transponder_save():
    """Сохранить настройки транспондера в transponder_config.json."""
    data = request.get_json(force=True) or {}
    base = load_transponder_config()
    merged = merge_transponder_request(base, data)
    saved = save_transponder_config(merged)
    return jsonify({"ok": True, "config": saved})


@app.route("/api/transponder/preview", methods=["POST"])
def api_transponder_preview():
    """NRZI hex для типов 18/19/24 без отправки (отправка — 127.0.0.1:6010)."""
    data = request.get_json(force=True) or {}
    base = load_transponder_config()
    cfg = merge_transponder_request(base, data)
    own = _fetch_ownship_from_hub()
    try:
        prev = build_preview(cfg, own)
    except RuntimeError as e:
        return jsonify({"ok": False, "error": str(e)}), 503
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 400
    prev.pop("dictionaries", None)
    return jsonify({"ok": True, "preview": prev})


@app.route("/api/transponder/send", methods=["POST"])
def api_transponder_send():
    """NRZI по UDP на 127.0.0.1:6010 с заголовком канал+слот (как тест слота)."""
    data = request.get_json(force=True) or {}
    which = data.get("which")
    if which not in ("18", "19", "24A", "24B", "broadcast"):
        return jsonify(
            {"ok": False, "error": "which must be 18, 19, 24A, 24B, broadcast"}
        ), 400

    cfg_body = {k: v for k, v in data.items() if k != "which"}
    base = load_transponder_config()
    cfg = merge_transponder_request(base, cfg_body)
    own = _fetch_ownship_from_hub()
    try:
        result = send_transmission(cfg, own, which)
    except RuntimeError as e:
        return jsonify({"ok": False, "error": str(e)}), 503
    except ValueError as e:
        return jsonify({"ok": False, "error": str(e)}), 400
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500
    return jsonify({"ok": True, "result": result})


@app.route("/api/transponder/gpio_pulse", methods=["POST"])
def api_transponder_gpio_pulse():
    """Один импульс TX (GPIO), без UDP. Настройки скрипта — из тела запроса или transponder_config."""
    data = request.get_json(force=True) or {}
    base = load_transponder_config()
    cfg = merge_transponder_request(base, data)
    pulse = tx_gpio_pulse(cfg, after_udp=False)
    if not pulse.get("ok"):
        err = pulse.get("error") or pulse.get("stderr") or "GPIO pulse failed"
        return jsonify({"ok": False, "error": err, "pulse": pulse}), 400
    return jsonify({"ok": True, "pulse": pulse})


@app.route("/api/transponder/send_raw", methods=["POST"])
def api_transponder_send_raw():
    """Сырой NRZI (hex) + канал + слот → UDP 127.0.0.1:6010."""
    data = request.get_json(force=True) or {}
    hx = data.get("nrzi_hex") or data.get("hex")
    if not isinstance(hx, str) or not hx.strip():
        return jsonify({"ok": False, "error": "nrzi_hex (hex строка) обязателен"}), 400
    channel = data.get("channel", "A")
    try:
        slot = int(data.get("slot", 0))
    except (TypeError, ValueError):
        return jsonify({"ok": False, "error": "slot must be integer 0..2249"}), 400
    if channel not in ("A", "B"):
        return jsonify({"ok": False, "error": "channel must be A or B"}), 400
    if slot < 0 or slot > 2249:
        return jsonify({"ok": False, "error": "slot must be 0..2249"}), 400
    try:
        body = parse_nrzi_hex(hx)
    except ValueError as e:
        return jsonify({"ok": False, "error": str(e)}), 400
    if not body:
        return jsonify({"ok": False, "error": "пустой payload после hex"}), 400
    cfg_body = {
        k: v
        for k, v in data.items()
        if k not in ("nrzi_hex", "hex", "channel", "slot")
    }
    base = load_transponder_config()
    cfg = merge_transponder_request(base, cfg_body)
    try:
        result = send_raw_nrzi_packet(channel, slot, body, cfg=cfg)
    except ValueError as e:
        return jsonify({"ok": False, "error": str(e)}), 400
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500
    return jsonify({"ok": True, "result": result})


# Аппаратно проверенная NRZI-последовательность для «Тест слота».
TEST_AIS_NRZI = bytes([
    102, 102, 102, 254, 149, 61, 224, 94, 245, 171, 174, 169,
    74, 84, 87, 105, 51, 82, 202, 166, 141, 99, 170, 170,
    170, 253, 236, 63, 170, 170, 170, 170,
])


@app.route("/api/send_test_slot", methods=["POST"])
def api_send_test_slot():
    """Отправляет тестовую датаграмму AIS NRZI в указанный слот/канал (UDP 127.0.0.1:6010)."""
    data = request.get_json(force=True)
    if not data:
        return jsonify({"ok": False, "error": "Empty payload"}), 400

    channel = data.get("channel")
    slot = data.get("slot")

    if channel not in ("A", "B"):
        return jsonify({"ok": False, "error": "channel must be 'A' or 'B'"}), 400
    if not isinstance(slot, int) or slot < 0 or slot > 2249:
        return jsonify({"ok": False, "error": "slot must be 0..2249"}), 400

    dest = ("127.0.0.1", 6010)
    datagram = build_slot_udp_payload(channel, slot, TEST_AIS_NRZI)

    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.sendto(datagram, dest)
        s.close()
        return jsonify({"ok": True, "dest": f"{dest[0]}:{dest[1]}", "size": len(datagram)})
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


# ==================== Network API ====================

@app.route("/api/network", methods=["GET"])
def api_network_get():
    """Returns current network configuration and live status."""
    with network_config_lock:
        cfg = load_network_config()
    live = get_current_network_info()
    return jsonify({"config": cfg, "live": live})


@app.route("/api/network", methods=["POST"])
def api_network_save():
    """Saves network configuration (does NOT switch mode immediately)."""
    data = request.get_json(force=True)
    if not data:
        return jsonify({"ok": False, "error": "Empty payload"}), 400

    with network_config_lock:
        cfg = load_network_config()
        for key in NETWORK_DEFAULTS:
            if key in data:
                cfg[key] = data[key]
        save_network_config(cfg)
    return jsonify({"ok": True, "config": cfg})


@app.route("/api/network/switch", methods=["POST"])
def api_network_switch():
    """Switches network mode to AP or WiFi by running the shell script."""
    data = request.get_json(force=True)
    target = data.get("mode") if data else None
    if target not in ("ap", "wifi"):
        return jsonify({"ok": False, "error": "Provide 'mode': 'ap' or 'wifi'"}), 400

    with network_config_lock:
        cfg = load_network_config()
        for key in NETWORK_DEFAULTS:
            if key in data and key != "mode":
                cfg[key] = data[key]
        save_network_config(cfg)

    result = switch_network_mode(target)
    return jsonify(result)


AIS_MINI_CONF = "/ais-mini.conf"
AIS_MINI_SERVICE = "aisMini.service"

AIS_HUB_CONF = "/opt/aishub/config/config.yaml"
AIS_HUB_SERVICE = "ais_hub.service"


@app.route("/api/config", methods=["GET"])
def api_config_get():
    """Returns the content of the AIS-catcher Mini configuration file."""
    try:
        with open(AIS_MINI_CONF, "r") as f:
            text = f.read()
        return jsonify({"ok": True, "text": text})
    except FileNotFoundError:
        return jsonify({"ok": False, "error": f"File not found: {AIS_MINI_CONF}"}), 404
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


@app.route("/api/config", methods=["POST"])
def api_config_save():
    """Saves the AIS-catcher Mini configuration file."""
    data = request.get_json(force=True)
    text = data.get("text") if data else None
    if text is None:
        return jsonify({"ok": False, "error": "No 'text' field"}), 400
    try:
        with open(AIS_MINI_CONF, "w") as f:
            f.write(text)
        return jsonify({"ok": True})
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


@app.route("/api/service/restart", methods=["POST"])
def api_service_restart():
    """Restarts the aisMini.service via systemctl."""
    try:
        result = subprocess.run(
            ["systemctl", "restart", AIS_MINI_SERVICE],
            capture_output=True, text=True, timeout=30,
        )
        if result.returncode == 0:
            return jsonify({"ok": True, "message": f"{AIS_MINI_SERVICE} restarted"})
        else:
            return jsonify({"ok": False, "error": result.stderr.strip() or f"Exit code {result.returncode}"}), 500
    except subprocess.TimeoutExpired:
        return jsonify({"ok": False, "error": "Restart timed out"}), 500
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


@app.route("/api/service/status", methods=["GET"])
def api_service_status():
    """Returns the status of aisMini.service."""
    try:
        result = subprocess.run(
            ["systemctl", "is-active", AIS_MINI_SERVICE],
            capture_output=True, text=True, timeout=5,
        )
        state = result.stdout.strip()
        return jsonify({"ok": True, "state": state})
    except Exception as e:
        return jsonify({"ok": True, "state": "unknown", "error": str(e)})


@app.route("/api/config/aishub", methods=["GET"])
def api_aishub_config_get():
    """Returns the content of the ais_hub configuration YAML."""
    try:
        with open(AIS_HUB_CONF, "r", encoding="utf-8", errors="replace") as f:
            text = f.read()
        return jsonify({"ok": True, "text": text})
    except FileNotFoundError:
        return jsonify({"ok": False, "error": f"File not found: {AIS_HUB_CONF}"}), 404
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


@app.route("/api/config/aishub", methods=["POST"])
def api_aishub_config_save():
    """Saves the ais_hub configuration YAML."""
    data = request.get_json(force=True)
    text = data.get("text") if data else None
    if text is None:
        return jsonify({"ok": False, "error": "No 'text' field"}), 400
    try:
        with open(AIS_HUB_CONF, "w", encoding="utf-8") as f:
            f.write(text)
        return jsonify({"ok": True})
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


@app.route("/api/service/aishub/status", methods=["GET"])
def api_aishub_service_status():
    """Returns the status of ais_hub.service."""
    try:
        result = subprocess.run(
            ["systemctl", "is-active", AIS_HUB_SERVICE],
            capture_output=True, text=True, timeout=5,
        )
        state = result.stdout.strip()
        return jsonify({"ok": True, "state": state})
    except Exception as e:
        return jsonify({"ok": True, "state": "unknown", "error": str(e)})


@app.route("/api/service/aishub/restart", methods=["POST"])
def api_aishub_service_restart():
    """Restarts ais_hub.service via systemctl."""
    try:
        result = subprocess.run(
            ["systemctl", "restart", AIS_HUB_SERVICE],
            capture_output=True, text=True, timeout=30,
        )
        if result.returncode == 0:
            return jsonify({"ok": True, "message": f"{AIS_HUB_SERVICE} restarted"})
        else:
            return jsonify({"ok": False, "error": result.stderr.strip() or f"Exit code {result.returncode}"}), 500
    except subprocess.TimeoutExpired:
        return jsonify({"ok": False, "error": "Restart timed out"}), 500
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


@app.route("/api/network/scan", methods=["GET"])
def api_network_scan():
    """Scans for available WiFi networks (Linux/iw only)."""
    cfg = load_network_config()
    iface = cfg.get("iface", "wlan0")
    try:
        out = subprocess.check_output(
            ["iw", "dev", iface, "scan", "ap-force"],
            timeout=15, stderr=subprocess.DEVNULL,
        ).decode()
        networks = []
        current = {}
        for line in out.splitlines():
            line = line.strip()
            if line.startswith("BSS "):
                if current.get("ssid"):
                    networks.append(current)
                current = {"bssid": line.split()[1].rstrip("("), "ssid": "", "signal": None}
            elif line.startswith("SSID:"):
                current["ssid"] = line.split(":", 1)[1].strip()
            elif line.startswith("signal:"):
                try:
                    current["signal"] = float(line.split(":")[1].strip().split()[0])
                except (ValueError, IndexError):
                    pass
        if current.get("ssid"):
            networks.append(current)
        seen = set()
        unique = []
        for n in networks:
            if n["ssid"] not in seen:
                seen.add(n["ssid"])
                unique.append(n)
        unique.sort(key=lambda x: x.get("signal") or -999, reverse=True)
        return jsonify({"ok": True, "networks": unique})
    except subprocess.TimeoutExpired:
        return jsonify({"ok": False, "error": "Scan timed out"})
    except Exception as e:
        return jsonify({"ok": False, "error": str(e), "networks": []})


# ==================== Certificate downloads ====================

@app.route("/ca.pem")
def download_ca():
    """Отдаёт корневой CA-сертификат для установки на клиентские устройства."""
    ca_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ca.pem")
    if not os.path.exists(ca_path):
        return "CA certificate not generated", 404
    return send_file(ca_path, mimetype="application/x-pem-file",
                     as_attachment=True, download_name="AISMap_CA.pem")


@app.route("/ca.crt")
def download_ca_crt():
    """Тот же CA, но с расширением .crt — Android открывает установщик автоматически."""
    ca_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ca.pem")
    if not os.path.exists(ca_path):
        return "CA certificate not generated", 404
    return send_file(ca_path, mimetype="application/x-x509-ca-cert",
                     as_attachment=True, download_name="AISMap_CA.crt")


# ==================== Static files with caching ====================

IMMUTABLE_YEAR = "public, max-age=31536000, immutable"
STATIC_MONTH = "public, max-age=2592000"
SHORT_REVALIDATE = "public, max-age=60, must-revalidate"


def _etag_for_path(path):
    try:
        st = os.stat(path)
        h = hashlib.md5(f"{st.st_mtime_ns}:{st.st_size}".encode("ascii")).hexdigest()
        return f'W/"{h}"'
    except Exception:
        return None


def _serve_with_cache(directory, filename, cache_control):
    """send_from_directory + заголовок Cache-Control + ETag/304."""
    abs_path = os.path.join(directory, filename)
    etag = _etag_for_path(abs_path)
    inm = request.headers.get("If-None-Match")
    if etag and inm and inm == etag:
        resp = Response(status=304)
        resp.headers["ETag"] = etag
        resp.headers["Cache-Control"] = cache_control
        return resp
    resp = send_from_directory(directory, filename)
    resp.headers["Cache-Control"] = cache_control
    if etag:
        resp.headers["ETag"] = etag
    return resp


@app.route("/svg/<path:filename>")
def serve_svg(filename):
    """SVG-иконки — редко меняются, долгий кеш."""
    return _serve_with_cache("SVG", filename, STATIC_MONTH)


@app.route("/static/leaflet/<path:filename>")
def serve_leaflet(filename):
    """Leaflet (версионирован по содержимому) — immutable."""
    return _serve_with_cache("static/leaflet", filename, IMMUTABLE_YEAR)


@app.route("/static/xterm/<path:filename>")
def serve_xterm(filename):
    """xterm.js — immutable."""
    return _serve_with_cache("static/xterm", filename, IMMUTABLE_YEAR)


@app.route("/static/js/<path:filename>")
def serve_static_js(filename):
    """app.js, модули — SWR: всегда проверяем обновление, но отдаём из кеша если не изменилось."""
    return _serve_with_cache("static/js", filename, SHORT_REVALIDATE)


@app.route("/static/css/<path:filename>")
def serve_static_css(filename):
    return _serve_with_cache("static/css", filename, SHORT_REVALIDATE)


@app.route("/sw.js")
def serve_sw():
    """Service Worker должен быть в корне для scope '/'."""
    path = os.path.join(os.path.dirname(__file__), "static", "sw.js")
    if not os.path.exists(path):
        return "sw.js not found", 404
    with open(path, "r", encoding="utf-8") as f:
        body = f.read()
    resp = Response(body, mimetype="application/javascript")
    resp.headers["Cache-Control"] = "no-store"
    resp.headers["Service-Worker-Allowed"] = "/"
    return resp


VECTOR_TILE_URL = os.environ.get("AISMAP_VECTOR_TILE_URL", "http://127.0.0.1:8080").rstrip("/")
VECTOR_TILE_SERVICE = os.environ.get("AISMAP_VECTOR_TILE_SERVICE", "planet_small_z14").strip().strip("/")


@app.route("/vtiles/<int:z>/<int:x>/<int:y>.pbf")
def proxy_vector_tile(z, x, y):
    """Проксирует PBF-тайлы с локального векторного тайлсервера. immutable: тайл не меняется."""
    try:
        candidates = [
            f"{VECTOR_TILE_URL}/services/{VECTOR_TILE_SERVICE}/tiles/{z}/{x}/{y}.pbf",
            f"{VECTOR_TILE_URL}/services/{VECTOR_TILE_SERVICE}/{z}/{x}/{y}.pbf",
            f"{VECTOR_TILE_URL}/data/{VECTOR_TILE_SERVICE}/{z}/{x}/{y}.pbf",
            f"{VECTOR_TILE_URL}/{VECTOR_TILE_SERVICE}/{z}/{x}/{y}.pbf",
        ]

        last_err = None
        for url in candidates:
            try:
                resp = urlopen(url, timeout=5)
                data = resp.read()
                code = getattr(resp, "status", 200) or 200
                headers = {
                    "Content-Type": resp.headers.get("Content-Type", "application/x-protobuf"),
                    "Cache-Control": IMMUTABLE_YEAR,
                    "Access-Control-Allow-Origin": "*",
                    "X-AISMap-Upstream": url,
                }
                ce = resp.headers.get("Content-Encoding")
                if ce:
                    headers["Content-Encoding"] = ce
                return data, code, headers
            except HTTPError as e:
                last_err = e
                if getattr(e, "code", None) != 404:
                    raise
            except URLError as e:
                last_err = e
                break

        if isinstance(last_err, HTTPError) and getattr(last_err, "code", None) == 404:
            return "Vector tile not found", 404
        return "Vector tile upstream unavailable", 503
    except (URLError, HTTPError):
        return "Vector tile upstream error", 503


@app.route("/tiles/<int:z>/<int:x>/<int:y>.png")
def serve_tile(z, x, y):
    """Отдаёт растровые тайлы из static/tiles с вечным кешем (immutable)."""
    max_tile = 2 ** z
    if x < 0 or x >= max_tile or y < 0 or y >= max_tile:
        return "Invalid tile coordinates", 404

    rel_dir = os.path.join("static", "tiles", str(z), str(x))
    tile_path = os.path.join(rel_dir, f"{y}.png")

    if not os.path.exists(tile_path):
        return "Tile not found", 404

    etag = _etag_for_path(tile_path)
    inm = request.headers.get("If-None-Match")
    if etag and inm and inm == etag:
        resp = Response(status=304)
        resp.headers["ETag"] = etag
        resp.headers["Cache-Control"] = IMMUTABLE_YEAR
        return resp

    resp = send_from_directory(rel_dir, f"{y}.png")
    resp.headers["Cache-Control"] = IMMUTABLE_YEAR
    if etag:
        resp.headers["ETag"] = etag
    return resp
