import logging
import random
import time

import requests
import tenacity
from stem import Signal
from stem.control import Controller

from datamarket.exceptions import EnsureNewIPTimeoutError, NoWorkingProxiesError

logger = logging.getLogger(__name__)
logging.getLogger("stem").setLevel(logging.WARNING)


class ProxyInterface:
    """
    Manage HTTP, HTTPS, and SOCKS5 proxies configured in the [proxy] section.
    """

    CHECK_IP_URL = "https://wtfismyip.com/json"

    def __init__(self, config):
        self._load_from_config(config)
        self.current_index = random.randrange(len(self.entries)) if self.entries else 0  # noqa: S311
        self._health = {}  # {entry: {"ok": bool, "last_checked": time.time(), "last_error": str}}

    def _load_from_config(self, cfg):
        # Tor password (optional)
        self.tor_password = cfg.get("proxy", "tor_password", fallback=None)

        # Comma-separated list of hosts
        hosts_raw = cfg.get("proxy", "hosts", fallback="")
        if not hosts_raw:
            raise RuntimeError("[proxy] hosts list is empty")

        entries = []
        for host_entry in (h.strip() for h in hosts_raw.split(",") if h.strip()):
            host, port, user, password = self._parse_host_entry(host_entry)
            entries.append((host, port, user, password))

        self.entries = entries

    def _parse_host_entry(self, host_entry):
        if "@" in host_entry:
            auth_part, host_part = host_entry.rsplit("@", 1)
            host, port = host_part.split(":")
            user, password = auth_part.split(":", 1)
            return host, port, user, password
        else:
            host, port = host_entry.split(":")
            return host, port, None, None

    @property
    def proxies(self):
        return self.get_proxies(use_tor=bool(self.tor_password))

    @staticmethod
    def get_proxy_url(host, port, user=None, password=None, schema="http"):
        auth = f"{user}:{password}@" if user and password else ""
        return f"{schema}://{auth}{host}:{port}"

    def get_proxies(
        self,
        use_tor=False,
        randomize=False,
        raw=False,
        use_auth=False,
        use_socks=False,
        health_check=True,
        check_timeout=5,
        cooldown_seconds=600,
        ensure_new_ip=False,
        ensure_new_ip_timeout=600,
        ensure_new_ip_interval=5,
        max_retry_seconds=600,
    ):
        """
        Return parsed proxy URLs or raw entry tuple for a working proxy.

        :param use_tor: route via local Tor SOCKS5 if True
        :param randomize: select a random proxy if True, otherwise round-robin
        :param raw: return raw (host, port, user, password) tuple if True
        :param use_auth: include proxies that require authentication if True; otherwise only credential-free
        :param health_check: perform health checks to ensure proxy is working if True
        :param check_timeout: timeout in seconds for health check requests
        :param cooldown_seconds: how long to cache health status before re-checking
        :param ensure_new_ip: if True and only one proxy available, wait until IP changes before returning
        :param ensure_new_ip_timeout: max seconds to wait for IP change when ensure_new_ip=True
        :param ensure_new_ip_interval: seconds between IP checks when ensure_new_ip=True
        :param max_retry_seconds: max seconds to retry finding working proxies (0 to disable)
        """
        # Tor handling (skip health check for tor)
        if use_tor:
            if raw:
                return ("127.0.0.1", "9050", None, None)
            return {"socks5": self.get_proxy_url("127.0.0.1", 9050, schema="socks5")}

        # Get a working entry (with health checks if enabled)
        if health_check:
            host, port, user, password = self._get_working_entry(
                use_auth=use_auth,
                randomize=randomize,
                check_timeout=check_timeout,
                cooldown_seconds=cooldown_seconds,
                ensure_new_ip=ensure_new_ip,
                ensure_new_ip_timeout=ensure_new_ip_timeout,
                ensure_new_ip_interval=ensure_new_ip_interval,
                max_retry_seconds=max_retry_seconds,
            )
        else:
            # Legacy behavior: no health check
            host, port, user, password = self.get_random(use_auth) if randomize else self.get_next(use_auth)

        if raw:
            return host, port, user, password

        # Build mapping of proxy URLs
        if use_socks:
            return {
                "socks5": self.get_proxy_url(host, port, user, password, "socks5"),
            }
        else:
            return {
                "http": self.get_proxy_url(host, port, user, password, "http"),
                "https": self.get_proxy_url(host, port, user, password, "http"),
            }

    def get_next(self, use_auth=False):
        # Round-robin selection, optionally filtering out authenticated proxies
        if not self.entries:
            raise NoWorkingProxiesError("No proxies available")

        pool = self.entries if use_auth else [e for e in self.entries if not e[2] and not e[3]]
        if not pool:
            pool = self.entries

        # Find next in pool using current_index
        for _ in range(len(self.entries)):
            idx = self.current_index
            self.current_index = (self.current_index + 1) % len(self.entries)
            entry = self.entries[idx]
            if entry in pool:
                return entry

        # Fallback to first entry
        return self.entries[0]

    def get_random(self, use_auth=False):
        # Random selection, optionally filtering out authenticated proxies
        if not self.entries:
            raise NoWorkingProxiesError("No proxies available")

        pool = self.entries if use_auth else [e for e in self.entries if not e[2] and not e[3]]
        if not pool:
            pool = self.entries

        entry = random.choice(pool)  # noqa: S311
        # Update index to after selected entry for round-robin continuity
        try:
            pos = self.entries.index(entry)
            self.current_index = (pos + 1) % len(self.entries)
        except ValueError:
            pass

        return entry

    def check_current_ip(self, proxies=None):
        try:
            proxies_arg = proxies or {"http": self.proxies["http"]}
            resp = requests.get(self.CHECK_IP_URL, proxies=proxies_arg, timeout=30)
            return resp.json().get("YourFuckingIPAddress")
        except Exception as ex:
            logger.error(ex)

    def renew_tor_ip(self):
        if not self.tor_password:
            logger.error("Tor password not configured")
            return

        try:
            logger.info(f"Current IP: {self.check_current_ip()}")
            with Controller.from_port(port=9051) as controller:
                controller.authenticate(password=self.tor_password)
                controller.signal(Signal.NEWNYM)

            time.sleep(5)
            logger.info(f"New IP: {self.check_current_ip()}")
        except Exception as ex:
            logger.error("Failed to renew Tor IP")
            logger.error(ex)

    def mark_entry_status(self, entry, ok, error=None, last_ip=None):
        """Update health cache for an entry."""
        self._health[entry] = {
            "ok": ok,
            "last_checked": time.time(),
            "last_error": error,
            "last_ip": last_ip,
        }

    def is_entry_alive(self, entry, timeout=5):
        """Check if a proxy entry is working by making a test request."""
        host, port, user, pwd = entry
        try:
            proxies = {
                "http": self.get_proxy_url(host, port, user, pwd, "http"),
                "https": self.get_proxy_url(host, port, user, pwd, "http"),
            }
            resp = requests.get(self.CHECK_IP_URL, proxies=proxies, timeout=timeout)
            ok = resp.status_code == 200
            last_ip = resp.json().get("YourFuckingIPAddress") if ok else None
            self.mark_entry_status(entry, ok, last_ip=last_ip)
            return ok
        except Exception as ex:
            self.mark_entry_status(entry, False, str(ex))
            return False

    def _get_working_entry(
        self,
        use_auth=False,
        randomize=False,
        check_timeout=5,
        cooldown_seconds=60,
        ensure_new_ip=False,
        ensure_new_ip_timeout=600,
        ensure_new_ip_interval=5,
        max_retry_seconds=600,
    ):
        """Get a working proxy entry, performing health checks as needed."""
        pool = self._build_pool(use_auth)
        candidates = self._get_candidates(pool, randomize)

        def _find_working_entry():
            if not self.entries:
                raise NoWorkingProxiesError("No proxies available")
            return self._find_working_entry_once(candidates, check_timeout, cooldown_seconds)

        if max_retry_seconds > 0:
            retrying = tenacity.Retrying(
                stop=tenacity.stop_after_delay(max_retry_seconds),
                reraise=True,
            )
            entry = retrying(_find_working_entry)
        else:
            entry = _find_working_entry()

        if ensure_new_ip and len(pool) == 1:
            logger.info(f"ensure_new_ip=True and single proxy, waiting for IP change: {entry[0]}:{entry[1]}")
            baseline = self._health.get(entry, {}).get("last_ip")
            if not baseline:
                if not self.is_entry_alive(entry, timeout=check_timeout):
                    raise NoWorkingProxiesError("Proxy became unavailable during ensure_new_ip")
                baseline = self._health.get(entry, {}).get("last_ip")
            entry = self._wait_for_new_ip(entry, baseline, ensure_new_ip_timeout, ensure_new_ip_interval, check_timeout)

        return entry

    def _get_round_robin_candidates(self, pool):
        """Get candidates in round-robin order starting from current_index."""
        candidates = []
        start_idx = self.current_index
        for i in range(len(self.entries)):
            idx = (start_idx + i) % len(self.entries)
            entry = self.entries[idx]
            if entry in pool:
                candidates.append(entry)
        # Update current_index for next call
        if candidates:
            try:
                pos = self.entries.index(candidates[0])
                self.current_index = (pos + 1) % len(self.entries)
            except ValueError:
                pass
        return candidates

    def _build_pool(self, use_auth):
        pool = self.entries if use_auth else [e for e in self.entries if not e[2] and not e[3]]
        if not pool:
            pool = self.entries
        return pool

    def _get_candidates(self, pool, randomize):
        if randomize:
            return pool[:]
        else:
            return self._get_round_robin_candidates(pool)

    def _find_working_entry_once(self, candidates, check_timeout, cooldown_seconds):
        for entry in candidates:
            health = self._health.get(entry, {})
            last_checked = health.get("last_checked", 0)
            ok = health.get("ok", False)
            now = time.time()

            if ok and (now - last_checked) < cooldown_seconds:
                logger.debug(f"Using cached working proxy: {entry[0]}:{entry[1]}")
                return entry
            elif not ok and (now - last_checked) < cooldown_seconds:
                continue
            else:
                logger.debug(f"Checking proxy health: {entry[0]}:{entry[1]}")
                if self.is_entry_alive(entry, timeout=check_timeout):
                    return entry

        logger.warning("No cached working proxies, forcing fresh checks")
        for entry in candidates:
            logger.debug(f"Force-checking proxy: {entry[0]}:{entry[1]}")
            if self.is_entry_alive(entry, timeout=check_timeout):
                return entry

        raise NoWorkingProxiesError("No working proxies available")

    def _wait_for_new_ip(self, entry, baseline, timeout, interval, check_timeout):
        start = time.time()
        while time.time() - start < timeout:
            host, port, user, pwd = entry
            proxies_map = {
                "http": self.get_proxy_url(host, port, user, pwd, "http"),
                "https": self.get_proxy_url(host, port, user, pwd, "http"),
            }
            try:
                resp = requests.get(self.CHECK_IP_URL, proxies=proxies_map, timeout=check_timeout)
                current_ip = resp.json().get("YourFuckingIPAddress")
            except Exception:
                current_ip = None

            if current_ip and current_ip != baseline:
                self.mark_entry_status(entry, True, last_ip=current_ip)
                logger.info(f"IP changed from {baseline} to {current_ip}")
                return entry

            time.sleep(interval)

        raise EnsureNewIPTimeoutError(f"Timed out waiting for new IP after {timeout}s")
