import json
import logging
import os
import random
import subprocess
import time
from pathlib import Path
from urllib.request import urlopen

_IP_SERVICES = (
    "https://api.ipify.org",
    "https://icanhazip.com",
    "https://ifconfig.me/ip",
)


def fetch_public_ip(timeout_sec: float = 5.0) -> str | None:
    for url in _IP_SERVICES:
        try:
            with urlopen(url, timeout=timeout_sec) as resp:
                ip = resp.read().decode("utf-8").strip()
                if ip:
                    return ip
        except Exception:
            continue
    return None


class WireGuardRotator:
    def __init__(
        self,
        config_dir: str | Path,
        *,
        cooldown_sec: float = 2.0,
        shuffle: bool = True,
        use_sudo: bool = True,
        wg_quick: str = "wg-quick",
    ) -> None:
        self.config_dir = Path(config_dir)
        self.cooldown_sec = cooldown_sec
        self.use_sudo = use_sudo
        self.wg_quick = wg_quick
        self._configs = self._load_configs(shuffle=shuffle)
        self._index = -1
        self.current_config: Path | None = None

    def _load_configs(self, *, shuffle: bool) -> list[Path]:
        if not self.config_dir.exists():
            raise FileNotFoundError(f"WG config dir not found: {self.config_dir}")
        configs = sorted(self.config_dir.glob("*.conf"))
        if not configs:
            raise FileNotFoundError(f"No .conf files found in {self.config_dir}")
        if shuffle:
            random.shuffle(configs)
        return configs

    def _run(self, args: list[str]) -> None:
        cmd = [self.wg_quick] + args
        if self.use_sudo:
            cmd = ["sudo"] + cmd
        logging.info("WG cmd: %s", " ".join(cmd))
        subprocess.run(cmd, check=True)
        self.log_public_ip()

    def up(self, config_path: Path) -> None:
        self._run(["up", str(config_path)])
        self.current_config = config_path
        if self.cooldown_sec:
            time.sleep(self.cooldown_sec)

    def down(self, config_path: Path) -> None:
        try:
            self._run(["down", str(config_path)])
        except Exception:
            logging.warning("WG down failed for %s; continuing", config_path)
        if self.cooldown_sec:
            time.sleep(self.cooldown_sec)

    def next_config(self) -> Path:
        self._index = (self._index + 1) % len(self._configs)
        return self._configs[self._index]

    def ensure_up(self) -> Path:
        if self.current_config is not None:
            return self.current_config
        config = self.next_config()
        self.up(config)
        return config

    def rotate(self, reason: str | None = None) -> Path:
        if reason:
            logging.warning("Rotating VPN: %s", reason)
        if self.current_config is not None:
            self.down(self.current_config)
        config = self.next_config()
        self.up(config)
        return config

    def log_public_ip(self, label: str = "VPN IP") -> str | None:
        ip = fetch_public_ip()
        if ip:
            logging.info("%s: %s", label, ip)
        else:
            logging.warning("%s: unavailable", label)
        return ip

    def shutdown(self) -> None:
        if self.current_config is not None:
            self.down(self.current_config)
            self.current_config = None


def parse_document_status_from_performance_logs(
    logs: list[dict],
    target_url: str | None = None,
) -> int | None:
    status = None
    for entry in logs:
        try:
            message = json.loads(entry.get("message", "{}")).get("message", {})
            if message.get("method") != "Network.responseReceived":
                continue
            params = message.get("params", {})
            if params.get("type") != "Document":
                continue
            response = params.get("response", {})
            url = response.get("url")
            if target_url and isinstance(url, str) and not url.startswith(target_url):
                continue
            status = response.get("status")
            if isinstance(status, int):
                return status
        except Exception:
            continue
    return status


def should_rotate_on_status(status_code: int | None) -> bool:
    return status_code is not None and status_code != 200


def is_dns_error_html(html: str) -> bool:
    if not html:
        return False
    markers = (
        "ERR_NAME_NOT_RESOLVED",
        "DNS_PROBE_POSSIBLE",
        "DNS_PROBE_FINISHED_NXDOMAIN",
        "dnsDefinition",
        "Адресу DNS",
        "IP-адресу сервера",
    )
    return any(m in html for m in markers)
