#!/usr/bin/env python3
import csv
import json
import re
import subprocess
import sys
from io import StringIO
from pathlib import Path

INSTANCE = sys.argv[1] if len(sys.argv) > 1 else "mgts-main"

BASE = Path("/opt/router-ops")
INSTANCE_DIR = BASE / "instances" / INSTANCE
EGRESS_YML = INSTANCE_DIR / "egress.yml"

PVE = "pve-mgts"
PVE_KEY = "/root/.ssh/pve_to_openwrt_mgts_ed25519"
VM101 = "10.71.100.2"

if not EGRESS_YML.exists():
    print(f"ERROR: missing {EGRESS_YML}", file=sys.stderr)
    sys.exit(1)


def header(title: str) -> None:
    print()
    print("################################################################")
    print(f"### {title}")
    print("################################################################")
    print()


def clean(v: str) -> str:
    return (v or "").strip().strip('"').strip("'")


def parse_sources_and_slots(text: str):
    sources = {}
    slots = []

    in_hmn = False
    hmn_indent = None
    for line in text.splitlines():
        if re.match(r"^\s+hidemyname:\s*$", line):
            in_hmn = True
            hmn_indent = len(line) - len(line.lstrip(" "))
            continue
        if in_hmn:
            stripped = line.strip()
            indent = len(line) - len(line.lstrip(" "))
            if stripped and indent <= hmn_indent and not stripped.startswith("#"):
                in_hmn = False
            else:
                for k in [
                    "candidate_source",
                    "selected_source",
                    "fallback_candidate_source",
                    "quarantine_source",
                    "legacy_empty_config_dir",
                ]:
                    m = re.match(rf"^\s*{re.escape(k)}:\s*(.+?)\s*$", line)
                    if m:
                        sources[k] = clean(m.group(1))

    in_slots = False
    current = None
    for line in text.splitlines():
        if re.match(r"^\s*slots:\s*$", line):
            in_slots = True
            continue

        if in_slots and re.match(r"^\s{2}[A-Za-z0-9_-]+:\s*$", line) and not line.strip().startswith("- "):
            if current:
                slots.append(current)
                current = None
            break

        if not in_slots:
            continue

        m = re.match(r"^\s*-\s*id:\s*(.+?)\s*$", line)
        if m:
            if current:
                slots.append(current)
            current = {"id": clean(m.group(1))}
            continue

        if current:
            m = re.match(r"^\s*([A-Za-z0-9_]+):\s*(.+?)\s*$", line)
            if m:
                current[m.group(1)] = clean(m.group(2))

    if current:
        slots.append(current)

    return sources, slots


def vm101_read(paths):
    script_lines = [
        "set -eu",
        "for f in " + " ".join("'" + p.replace("'", "'\"'\"'") + "'" for p in paths) + "; do",
        "  echo __BEGIN_FILE__:$f",
        "  if [ -f \"$f\" ]; then",
        "    wc -l < \"$f\" | sed 's/^/__LINES__:/'",
        "    sed -n '1,1000p' \"$f\" | sed -E 's#([A-Za-z0-9+/=]{32,})#[REDACTED_LONG_TOKEN]#g'",
        "  else",
        "    echo __MISSING__",
        "  fi",
        "  echo __END_FILE__:$f",
        "done",
    ]
    script = "\n".join(script_lines) + "\n"

    cmd = [
        "ssh",
        "-o", "BatchMode=yes",
        "-o", "ConnectTimeout=10",
        PVE,
        f"ssh -i {PVE_KEY} -o BatchMode=yes -o ConnectTimeout=8 root@{VM101} 'sh -s'",
    ]

    p = subprocess.run(cmd, input=script, text=True, capture_output=True)
    if p.returncode != 0:
        print(p.stdout, end="")
        print(p.stderr, end="", file=sys.stderr)
        raise SystemExit(p.returncode)

    files = {}
    current = None
    buf = []
    for line in p.stdout.splitlines():
        if line.startswith("__BEGIN_FILE__:"):
            current = line.split(":", 1)[1]
            buf = []
            continue
        if line.startswith("__END_FILE__:"):
            files[current] = "\n".join(buf) + ("\n" if buf else "")
            current = None
            buf = []
            continue
        if current is not None:
            buf.append(line)

    return files


def split_tsv(text: str):
    rows_raw = []
    line_count = None
    missing = False

    for line in text.splitlines():
        if line.startswith("__LINES__:"):
            try:
                line_count = int(line.split(":", 1)[1])
            except Exception:
                line_count = None
            continue
        if line == "__MISSING__":
            missing = True
            continue
        if line.strip():
            rows_raw.append(line)

    if missing or not rows_raw:
        return line_count, []

    reader = csv.DictReader(StringIO("\n".join(rows_raw) + "\n"), delimiter="\t")
    rows = []
    for row in reader:
        rows.append({(k or "").strip(): (v or "").strip() for k, v in row.items()})
    return line_count, rows


def pick(row, *names):
    lower = {k.lower(): v for k, v in row.items()}
    for name in names:
        v = lower.get(name.lower())
        if v:
            return v
    return ""


def basename(path: str) -> str:
    return (path or "").rstrip("/").split("/")[-1]


def norm_candidate(row, source_name):
    fn_raw = pick(row, "file", "config", "config_path", "path")
    slot = pick(row, "slot", "id", "name")
    ep = pick(row, "endpoint")
    avg = pick(row, "avg_ms", "avg", "rtt", "latency", "ping_rtt_avg")
    try:
        avg_float = float(avg)
    except Exception:
        avg_float = 999999.0

    return {
        "source": source_name,
        "slot": slot,
        "endpoint": ep,
        "avg_ms": avg,
        "avg_sort": avg_float,
        "country": pick(row, "country", "Country"),
        "city": pick(row, "city", "City"),
        "file": basename(fn_raw),
    }


def row_key(c):
    return (c.get("endpoint") or "", c.get("file") or "")


sources, slots = parse_sources_and_slots(EGRESS_YML.read_text())

candidate_source = sources.get("candidate_source", "/root/hmn/cache/ok-awg1-strict-foreign-latest.tsv")
selected_source = sources.get("selected_source", "/root/hmn/cache/selected-awg1-latest.tsv")
fallback_source = sources.get("fallback_candidate_source", "/root/hmn/cache/ok-awg1-strict-all-latest.tsv")
quarantine_source = sources.get("quarantine_source", "/root/hmn/cache/quarantine-awg1-latest.tsv")

paths = [candidate_source, selected_source, fallback_source, quarantine_source]
files = vm101_read(paths)

parsed = {}
for path in paths:
    line_count, rows = split_tsv(files.get(path, ""))
    parsed[path] = {"line_count": line_count, "rows": rows}

primary = [norm_candidate(r, "primary") for r in parsed[candidate_source]["rows"]]
selected = [norm_candidate(r, "selected") for r in parsed[selected_source]["rows"]]
fallback = [norm_candidate(r, "fallback") for r in parsed[fallback_source]["rows"]]
quarantine = [norm_candidate(r, "quarantine") for r in parsed[quarantine_source]["rows"]]

quarantine_keys = {row_key(c) for c in quarantine}
selected_by_slot = {}
for c in selected:
    if c["slot"]:
        selected_by_slot[c["slot"]] = c

selected_keys = {row_key(c) for c in selected}

def is_quarantined(c):
    return row_key(c) in quarantine_keys

def is_same(a, b):
    return row_key(a) == row_key(b)

primary_clean = [c for c in primary if not is_quarantined(c)]
primary_clean.sort(key=lambda c: c["avg_sort"])

# Strategy 1: preserve existing slot assignments where known.
preserve_plan = []
used = set()
for s in slots:
    iface = s.get("iface", "")
    candidate = None
    reason = ""

    if iface in selected_by_slot:
        candidate = selected_by_slot[iface]
        reason = "preserve_existing_selected"
    else:
        for c in primary_clean:
            if row_key(c) not in used:
                candidate = c
                reason = "fill_from_primary"
                break

    if candidate:
        used.add(row_key(candidate))

    preserve_plan.append({
        "slot_id": s.get("id", ""),
        "iface": iface,
        "table": s.get("table", ""),
        "fwmark": s.get("fwmark", ""),
        "candidate": candidate,
        "reason": reason or "none",
    })

# Strategy 2: pure best latency from primary.
best_plan = []
used2 = set()
for s in slots:
    candidate = None
    reason = ""
    for c in primary_clean:
        if row_key(c) not in used2:
            candidate = c
            reason = "best_latency_primary"
            break
    if candidate:
        used2.add(row_key(candidate))
    best_plan.append({
        "slot_id": s.get("id", ""),
        "iface": s.get("iface", ""),
        "table": s.get("table", ""),
        "fwmark": s.get("fwmark", ""),
        "candidate": candidate,
        "reason": reason or "none",
    })

def print_plan(title, plan):
    header(title)
    print(f"{'slot':<12} {'iface':<6} {'table':<5} {'fwmark':<7} {'reason':<28} {'endpoint':<28} {'avg':<8} file")
    for p in plan:
        c = p["candidate"] or {}
        print(
            f"{p['slot_id']:<12} {p['iface']:<6} {p['table']:<5} {p['fwmark']:<7} "
            f"{p['reason']:<28} {c.get('endpoint',''):<28} {c.get('avg_ms',''):<8} {c.get('file','')}"
        )

header("HMN ADAPTER PLAN")
print(f"INSTANCE={INSTANCE}")
print("MODE=READ_ONLY")
print("REMOTE_CHANGES=NO")
print()

for name, path in [
    ("candidate_source", candidate_source),
    ("selected_source", selected_source),
    ("fallback_source", fallback_source),
    ("quarantine_source", quarantine_source),
]:
    print(f"{name:<22} {path:<62} rows={len(parsed[path]['rows'])} lines={parsed[path]['line_count']}")

header("CURRENT LEGACY SELECTED")
print(f"{'slot':<8} {'endpoint':<28} {'avg':<8} file")
for c in selected:
    print(f"{c['slot']:<8} {c['endpoint']:<28} {c['avg_ms']:<8} {c['file']}")

header("PRIMARY CANDIDATE POOL")
print(f"primary_total={len(primary)} primary_clean_non_quarantine={len(primary_clean)} quarantine={len(quarantine)}")
print(f"{'rank':<5} {'endpoint':<28} {'avg':<8} {'selected':<9} file")
for i, c in enumerate(primary_clean, 1):
    sel = "yes" if row_key(c) in selected_keys else "no"
    print(f"{i:<5} {c['endpoint']:<28} {c['avg_ms']:<8} {sel:<9} {c['file']}")

print_plan("STRATEGY A: PRESERVE EXISTING, RECOMMENDED FIRST APPLY", preserve_plan)
print_plan("STRATEGY B: BEST LATENCY, NOT FIRST APPLY", best_plan)

header("JSON SUMMARY")
summary = {
    "instance": INSTANCE,
    "mode": "READ_ONLY",
    "remote_changes": False,
    "sources": {
        "candidate_source": candidate_source,
        "selected_source": selected_source,
        "fallback_source": fallback_source,
        "quarantine_source": quarantine_source,
    },
    "counts": {
        "primary": len(primary),
        "primary_clean_non_quarantine": len(primary_clean),
        "selected": len(selected),
        "fallback": len(fallback),
        "quarantine": len(quarantine),
    },
    "recommended_initial_strategy": "preserve_existing",
    "preserve_existing_plan": preserve_plan,
    "best_latency_plan": best_plan,
}
# Drop avg_sort internal field from JSON.
def strip_internal(obj):
    if isinstance(obj, dict):
        return {k: strip_internal(v) for k, v in obj.items() if k != "avg_sort"}
    if isinstance(obj, list):
        return [strip_internal(x) for x in obj]
    return obj

print(json.dumps(strip_internal(summary), ensure_ascii=False, indent=2))

header("ASSESSMENT")
print("No changes applied.")
print("Recommended initial apply strategy: preserve_existing.")
print("Reason: keep current legacy vpn1/vpn2 endpoint assignments stable, then fill vpn3-vpn5 from primary candidates.")
print("Next: STEP_030F can create a VM101 apply preflight that checks config availability for the proposed candidates without changing interfaces.")
