import os
import json
import shutil
import socket
import subprocess
import time


BASE_DIR = os.path.dirname(os.path.dirname(__file__))
STATE_DIR = os.path.join(BASE_DIR, ".cache")
SETUP_INFO_FILE = os.path.join(STATE_DIR, "wifi_setup_mode.json")
CONNECT_RESULT_FILE = os.path.join(STATE_DIR, "wifi_connect_result.json")
SCAN_CACHE_FILE = os.path.join(STATE_DIR, "wifi_scan_cache.json")
CONNECTION_NAME = "its-a-plane-setup"
SSID_PREFIX = "PlaneTracker-Setup"
DEFAULT_PASSWORD = "planes12345"
SETUP_IP = "10.42.0.1"


def _run(cmd, timeout=20):
    try:
        return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        return subprocess.CompletedProcess(
            cmd,
            124,
            stdout="",
            stderr=f"Command timed out after {timeout} seconds",
        )
    except OSError as exc:
        return subprocess.CompletedProcess(cmd, 127, stdout="", stderr=str(exc))


def _nmcli_cmd(cmd):
    if shutil.which("nmcli"):
        return ["nmcli", *cmd]
    return None


def _run_nmcli(cmd, timeout=20, allow_sudo=True):
    base = _nmcli_cmd(cmd)
    if not base:
        return None
    result = _run(base, timeout=timeout)
    if result.returncode != 0 and allow_sudo and os.name != "nt" and shutil.which("sudo"):
        retry = _run(["sudo", "-n", *base], timeout=timeout)
        if retry.returncode == 0:
            return retry
    return result


def _split_nmcli(line):
    parts = []
    current = []
    escape = False
    for char in line.rstrip("\n"):
        if escape:
            current.append(char)
            escape = False
        elif char == "\\":
            escape = True
        elif char == ":":
            parts.append("".join(current))
            current = []
        else:
            current.append(char)
    parts.append("".join(current))
    return parts


def _merge_scanned_network(networks, ssid, signal=0, security="", source="pre-hotspot"):
    ssid = str(ssid or "").strip()
    if not ssid or ssid.startswith(SSID_PREFIX):
        return
    try:
        signal = int(signal)
    except (TypeError, ValueError):
        signal = 0
    existing = networks.get(ssid)
    if not existing or signal > existing["signal"]:
        networks[ssid] = {
            "ssid": ssid,
            "signal": signal,
            "security": security,
            "active": False,
            "source": source,
        }


def _scan_with_iwlist(device):
    iwlist = shutil.which("iwlist")
    if not iwlist:
        return []
    cmd = [iwlist, device, "scan"]
    result = _run(cmd, timeout=30)
    if result.returncode != 0 and os.name != "nt" and shutil.which("sudo"):
        result = _run(["sudo", "-n", *cmd], timeout=30)
    if result.returncode != 0:
        return []

    networks = []
    current = None
    for raw_line in result.stdout.splitlines():
        line = raw_line.strip()
        if line.startswith("Cell "):
            if current:
                networks.append(current)
            current = {"ssid": "", "signal": 0, "security": ""}
        elif current is not None and "ESSID:" in line:
            current["ssid"] = line.split("ESSID:", 1)[1].strip().strip('"')
        elif current is not None and "Quality=" in line:
            quality = line.split("Quality=", 1)[1].split()[0]
            try:
                value, maximum = quality.split("/", 1)
                current["signal"] = round(100 * float(value) / float(maximum))
            except (TypeError, ValueError, ZeroDivisionError):
                pass
        elif current is not None and line.startswith("Encryption key:"):
            current["security"] = "" if line.endswith(":off") else "SECURED"
        elif current is not None and ("WPA" in line or "RSN" in line):
            current["security"] = "WPA"
    if current:
        networks.append(current)
    return networks


def _scan_networks_before_hotspot(device):
    """Cache nearby networks while the radio is still free to scan."""
    if not device:
        return []

    _run_nmcli(["radio", "wifi", "on"], timeout=10)
    _run_nmcli(["device", "set", device, "managed", "yes"], timeout=10)

    ready_deadline = time.time() + 20
    while time.time() < ready_deadline:
        status = _run_nmcli(
            ["-g", "GENERAL.STATE", "device", "show", device],
            timeout=8,
            allow_sudo=False,
        )
        state = status.stdout.strip().lower() if status and status.returncode == 0 else ""
        if state and "unavailable" not in state and "unmanaged" not in state:
            break
        time.sleep(2)

    networks = {}
    for attempt in range(3):
        rescan = _run_nmcli(
            ["device", "wifi", "rescan", "ifname", device],
            timeout=20,
        )
        if not rescan or rescan.returncode != 0:
            time.sleep(2)
            continue
        time.sleep(4)
        result = _run_nmcli(
            [
                "-t", "-f", "SSID,SIGNAL,SECURITY",
                "device", "wifi", "list", "ifname", device,
            ],
            timeout=20,
        )
        if result and result.returncode == 0:
            for line in result.stdout.splitlines():
                parts = _split_nmcli(line)
                if len(parts) >= 3:
                    _merge_scanned_network(
                        networks,
                        parts[0],
                        parts[1],
                        parts[2],
                        source="pre-hotspot-nmcli",
                    )
        if attempt < 2:
            time.sleep(2)

    for network in _scan_with_iwlist(device):
        _merge_scanned_network(
            networks,
            network.get("ssid"),
            network.get("signal"),
            network.get("security"),
            source="pre-hotspot-iwlist",
        )

    print(f"[WiFiSetup] Pre-hotspot scan found {len(networks)} network(s)")
    payload = {
        "scanned_at": int(time.time()),
        "networks": sorted(
            networks.values(),
            key=lambda item: item["signal"],
            reverse=True,
        ),
    }
    try:
        os.makedirs(STATE_DIR, exist_ok=True)
        with open(SCAN_CACHE_FILE, "w", encoding="utf-8") as f:
            json.dump(payload, f, indent=2)
    except OSError:
        pass
    return payload["networks"]


def cached_scan_networks(max_age_seconds=600):
    try:
        with open(SCAN_CACHE_FILE, "r", encoding="utf-8") as f:
            payload = json.load(f)
        age = time.time() - float(payload.get("scanned_at") or 0)
        if age > max_age_seconds:
            return []
        return payload.get("networks") or []
    except (OSError, TypeError, ValueError):
        return []


def wifi_device():
    result = _run_nmcli(["-t", "-f", "DEVICE,TYPE", "device", "status"], timeout=8, allow_sudo=False)
    if result and result.returncode == 0:
        for line in result.stdout.splitlines():
            parts = _split_nmcli(line)
            if len(parts) >= 2 and parts[1] == "wifi" and not parts[0].startswith("p2p-"):
                return parts[0]
    return "wlan0"


def active_wifi_ssid():
    result = _run_nmcli(["-t", "-f", "ACTIVE,SSID", "device", "wifi", "list"], timeout=8, allow_sudo=False)
    if result and result.returncode == 0:
        for line in result.stdout.splitlines():
            parts = _split_nmcli(line)
            if len(parts) >= 2 and parts[0] == "yes":
                return parts[1]
    return ""


def active_wifi_ip():
    device = wifi_device()
    result = _run_nmcli(
        ["-g", "IP4.ADDRESS", "device", "show", device],
        timeout=8,
        allow_sudo=False,
    )
    if result and result.returncode == 0:
        for line in result.stdout.splitlines():
            address = line.strip().split("/", 1)[0]
            if address and address != SETUP_IP and not address.startswith("127."):
                return address
    return ""


def setup_ssid():
    suffix = ""
    try:
        hostname = socket.gethostname().strip()
        if hostname:
            suffix = "-" + hostname[-6:].replace("_", "-")
    except Exception:
        pass
    return (SSID_PREFIX + suffix)[:32]


def is_setup_active():
    return active_wifi_ssid().startswith(SSID_PREFIX)


def _known_non_setup_connections():
    result = _run_nmcli(["-t", "-f", "NAME,TYPE,AUTOCONNECT", "connection", "show"], timeout=8, allow_sudo=False)
    if not result or result.returncode != 0:
        return []
    names = []
    for line in result.stdout.splitlines():
        parts = _split_nmcli(line)
        if len(parts) >= 2 and parts[1] == "802-11-wireless" and parts[0] != CONNECTION_NAME:
            names.append(parts[0])
    return names


def wait_for_known_wifi(seconds=35):
    deadline = time.time() + max(seconds, 0)
    while time.time() < deadline:
        ssid = active_wifi_ssid()
        if ssid and not ssid.startswith(SSID_PREFIX):
            return ssid
        time.sleep(2)
    return ""


def start_setup_ap(force=False):
    os.makedirs(STATE_DIR, exist_ok=True)
    current = active_wifi_ssid()
    if current and not current.startswith(SSID_PREFIX) and not force:
        return {"active": False, "started": False, "ssid": current, "message": f"Already connected to {current}"}

    device = wifi_device()
    ssid = setup_ssid()
    password = os.environ.get("ITS_A_PLANE_SETUP_WIFI_PASSWORD", DEFAULT_PASSWORD)

    _run_nmcli(["connection", "delete", CONNECTION_NAME], timeout=10)
    _scan_networks_before_hotspot(device)
    result = _run_nmcli(
        [
            "device", "wifi", "hotspot",
            "ifname", device,
            "con-name", CONNECTION_NAME,
            "ssid", ssid,
            "password", password,
        ],
        timeout=35,
    )
    ok = bool(result and result.returncode == 0)
    if ok:
        _run_nmcli(["connection", "modify", CONNECTION_NAME, "connection.autoconnect", "no"], timeout=10)

    info = {
        "active": ok,
        "started": ok,
        "ssid": ssid,
        "password": password,
        "ip": SETUP_IP,
        "url": f"http://{SETUP_IP}:8080/wifi-setup",
        "device": device,
        "message": "Setup hotspot active" if ok else ((result.stderr or result.stdout).strip() if result else "nmcli is not available"),
    }
    try:
        with open(SETUP_INFO_FILE, "w", encoding="utf-8") as f:
            json.dump(info, f, indent=2)
    except Exception:
        pass
    return info


def stop_setup_ap():
    result = _run_nmcli(["connection", "down", CONNECTION_NAME], timeout=15)
    _run_nmcli(["connection", "delete", CONNECTION_NAME], timeout=10)
    return result.returncode == 0 if result else False


def _write_connect_result(data):
    os.makedirs(STATE_DIR, exist_ok=True)
    try:
        import json
        with open(CONNECT_RESULT_FILE, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)
    except Exception:
        pass


def _profile_exists(name):
    result = _run_nmcli(["-t", "-f", "NAME", "connection", "show"], timeout=10)
    if not result or result.returncode != 0:
        return False
    return name in [line.strip() for line in result.stdout.splitlines()]


def _delete_connection(name):
    if name:
        _run_nmcli(["connection", "delete", name], timeout=15)


def _scan_for_ssid(device, ssid, attempts=4):
    for _ in range(max(attempts, 1)):
        _run_nmcli(["device", "wifi", "rescan", "ifname", device], timeout=20)
        time.sleep(3)
        result = _run_nmcli(
            ["-t", "-f", "SSID", "device", "wifi", "list", "ifname", device],
            timeout=15,
        )
        if result and result.returncode == 0:
            for line in result.stdout.splitlines():
                if _split_nmcli(line)[0] == ssid:
                    return True
    return False


def _manual_wifi_profile(device, ssid, password):
    con_name = ssid
    _delete_connection(con_name)
    add = _run_nmcli(
        [
            "connection", "add",
            "type", "wifi",
            "ifname", device,
            "con-name", con_name,
            "ssid", ssid,
        ],
        timeout=20,
    )
    if not add or add.returncode != 0:
        return add
    if password:
        sec = _run_nmcli(
            [
                "connection", "modify", con_name,
                "wifi-sec.key-mgmt", "wpa-psk",
                "wifi-sec.psk", password,
            ],
            timeout=20,
        )
        if not sec or sec.returncode != 0:
            return sec
    else:
        _run_nmcli(["connection", "modify", con_name, "wifi-sec.key-mgmt", ""], timeout=10)
    _run_nmcli(
        [
            "connection", "modify", con_name,
            "connection.autoconnect", "yes",
            "connection.autoconnect-priority", "100",
        ],
        timeout=15,
    )
    return _run_nmcli(["connection", "up", con_name], timeout=50)


def connect_to_wifi(ssid, password=""):
    ssid = str(ssid or "").strip()
    if not ssid:
        return {"ok": False, "message": "SSID is required."}

    device = wifi_device()
    was_setup = is_setup_active()
    _write_connect_result({"ok": None, "state": "starting", "ssid": ssid})

    if was_setup:
        stop_setup_ap()
        time.sleep(5)

    _run_nmcli(["radio", "wifi", "on"], timeout=10)
    _run_nmcli(["device", "set", device, "managed", "yes"], timeout=10)
    _run_nmcli(["device", "disconnect", device], timeout=10)
    time.sleep(2)

    # Remove stale or incomplete profiles for this SSID; they can cause
    # NetworkManager errors such as "wifi-sec.key-mgmt: property is missing".
    _delete_connection(ssid)
    if _profile_exists(CONNECTION_NAME):
        _delete_connection(CONNECTION_NAME)

    _scan_for_ssid(device, ssid)

    cmd = ["device", "wifi", "connect", ssid]
    if password:
        cmd.extend(["password", password])
    if device:
        cmd.extend(["ifname", device])
    result = _run_nmcli(cmd, timeout=50)

    if not result or result.returncode != 0:
        result = _manual_wifi_profile(device, ssid, password)

    if result and result.returncode == 0:
        connection_name = ssid
        active = _run_nmcli(["-g", "GENERAL.CONNECTION", "device", "show", device], timeout=10)
        if active and active.returncode == 0 and active.stdout.strip():
            connection_name = active.stdout.strip()
        _run_nmcli(
            [
                "connection", "modify", connection_name,
                "connection.autoconnect", "yes",
                "connection.autoconnect-priority", "100",
            ],
            timeout=15,
        )
        output = {
            "ok": True,
            "state": "connected",
            "ssid": ssid,
            "connection": connection_name,
            "message": f"Connected to {ssid}.",
        }
        _write_connect_result(output)
        return output

    error = ""
    if result:
        error = (result.stderr or result.stdout).strip()
    output = {
        "ok": False,
        "state": "failed",
        "ssid": ssid,
        "message": error or f"Could not connect to {ssid}.",
    }
    _write_connect_result(output)

    if was_setup:
        time.sleep(2)
        restored = start_setup_ap(force=True)
        output["setup_restored"] = bool(restored.get("active"))
        _write_connect_result(output)
    return output


def connection_result():
    try:
        import json
        with open(CONNECT_RESULT_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return {"ok": None, "state": "idle", "message": ""}


def ensure_setup_ap_if_offline(wait_seconds=35):
    connected = wait_for_known_wifi(wait_seconds)
    if connected:
        return {"active": False, "started": False, "ssid": connected, "message": f"Connected to {connected}"}
    return start_setup_ap()


def status():
    ssid = active_wifi_ssid()
    setup = ssid.startswith(SSID_PREFIX) if ssid else False
    return {
        "active": setup,
        "ssid": ssid,
        "setup_ssid": setup_ssid(),
        "password": os.environ.get("ITS_A_PLANE_SETUP_WIFI_PASSWORD", DEFAULT_PASSWORD),
        "ip": SETUP_IP,
        "url": f"http://{SETUP_IP}:8080/wifi-setup",
        "known_connections": _known_non_setup_connections(),
    }
