"""Server-side scan quota check via the Oncecheck API."""

from __future__ import annotations

import json
import urllib.error
import urllib.request
from typing import TypedDict

from .. import __version__

WEB_APP_URL = "https://www.oncecheck.com"


class QuotaResult(TypedDict):
    allowed: bool
    plan: str
    used: int
    limit: int
    allowed_rules: list[str] | None  # None = all rules (team), list = restricted (starter)
    allowed_engines: list[str]  # e.g. ["heuristic"] or ["heuristic", "codeql", "semgrep"]


class ProcessedScan(TypedDict):
    platform: str
    project_type: str
    findings: list[dict]
    gated_count: int


class ProcessResult(TypedDict):
    plan: str
    scans: list[ProcessedScan]


_PLAN_DAILY_LIMITS = {
    "starter": 3,
    "team": -1,  # unlimited
}


class _PostRedirectHandler(urllib.request.HTTPRedirectHandler):
    """Follow 307/308 redirects while preserving the POST method and body."""

    def redirect_request(
        self, req, fp, code, msg, headers, newurl  # noqa: ANN001
    ):
        if code in (307, 308):
            new_req = urllib.request.Request(
                newurl,
                data=req.data,
                method=req.get_method(),
                headers=dict(req.headers),
            )
            return new_req
        return super().redirect_request(req, fp, code, msg, headers, newurl)


_opener = urllib.request.build_opener(_PostRedirectHandler)


def _safe_int(value: object, default: int) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def normalize_quota_result(raw: dict, fallback_plan: str = "starter") -> QuotaResult:
    """Normalize server quota payload and apply defensive enforcement rules."""
    plan = str(raw.get("plan") or fallback_plan or "starter").strip().lower()
    if plan not in _PLAN_DAILY_LIMITS:
        plan = str(fallback_plan or "starter").strip().lower()
    if plan not in _PLAN_DAILY_LIMITS:
        plan = "starter"

    expected_limit = _PLAN_DAILY_LIMITS[plan]
    used = max(0, _safe_int(raw.get("used"), 0))
    limit = _safe_int(raw.get("limit"), expected_limit)

    # Prevent accidental unlimited quota for non-unlimited tiers due to bad server values.
    if expected_limit >= 0 and limit < 0:
        limit = expected_limit

    allowed = bool(raw.get("allowed", True))
    if limit >= 0 and used >= limit:
        allowed = False

    # Server-provided allowed rules (None = all rules allowed)
    raw_rules = raw.get("allowed_rules")
    if isinstance(raw_rules, list):
        allowed_rules: list[str] | None = [str(r) for r in raw_rules if isinstance(r, str)]
    else:
        allowed_rules = None

    # Server-provided allowed engines
    raw_engines = raw.get("allowed_engines")
    if isinstance(raw_engines, list) and raw_engines:
        allowed_engines = [str(e) for e in raw_engines if isinstance(e, str)]
    else:
        allowed_engines = ["heuristic"]  # safe default: most restrictive

    return {
        "allowed": allowed,
        "plan": plan,
        "used": used,
        "limit": limit,
        "allowed_rules": allowed_rules,
        "allowed_engines": allowed_engines,
    }


def _call_scan_api(access_token: str, record: bool = False, fallback_plan: str = "starter") -> QuotaResult:
    """Call the scan-check API. Set record=True to increment the counter."""
    url = f"{WEB_APP_URL}/api/scan-check"
    payload = json.dumps({"record": record}).encode()
    req = urllib.request.Request(
        url,
        data=payload,
        method="POST",
        headers={
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
            "User-Agent": f"oncecheck-cli/{__version__}",
        },
    )

    try:
        with _opener.open(req, timeout=10) as resp:
            payload = json.loads(resp.read())
            if not isinstance(payload, dict):
                raise ConnectionError("Scan check failed: invalid response payload")
            return normalize_quota_result(payload, fallback_plan=fallback_plan)
    except urllib.error.HTTPError as exc:
        if exc.code == 401:
            raise PermissionError("Token expired or invalid. Run `oncecheck login` to re-authenticate.")
        body = exc.read().decode(errors="replace")
        raise ConnectionError(f"Scan check failed ({exc.code}): {body}")
    except (urllib.error.URLError, TimeoutError, OSError) as exc:
        raise ConnectionError(f"Could not reach Oncecheck server: {exc}")


def check_scan_quota(access_token: str, fallback_plan: str = "starter") -> QuotaResult:
    """Check scan quota without incrementing the counter."""
    return _call_scan_api(access_token, record=False, fallback_plan=fallback_plan)


def record_scan(access_token: str, fallback_plan: str = "starter") -> QuotaResult:
    """Record a completed scan (increments the counter)."""
    return _call_scan_api(access_token, record=True, fallback_plan=fallback_plan)


def process_findings(access_token: str, scans: list[dict], progress_callback=None) -> ProcessResult:
    """Send raw findings to the server for plan-based filtering and scan recording.

    The server filters findings to the user's allowed rules, records the scan,
    and returns only the findings the plan permits.
    """
    url = f"{WEB_APP_URL}/api/scan-process"
    payload = json.dumps({"scans": scans}).encode()
    if progress_callback:
        total = sum(len(s.get("findings", [])) for s in scans)
        size_kb = len(payload) / 1024
        progress_callback(f"Uploading {total} findings ({size_kb:.0f} KB)...")
    req = urllib.request.Request(
        url,
        data=payload,
        method="POST",
        headers={
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
            "User-Agent": f"oncecheck-cli/{__version__}",
        },
    )

    try:
        with _opener.open(req, timeout=30) as resp:
            data = json.loads(resp.read())
            if not isinstance(data, dict):
                raise ConnectionError("Scan processing failed: invalid response")
            return data
    except urllib.error.HTTPError as exc:
        if exc.code == 401:
            raise PermissionError("Token expired or invalid. Run `oncecheck login` to re-authenticate.")
        if exc.code == 403:
            raise PermissionError("Scan limit reached. Upgrade to Team for unlimited scans.")
        body = exc.read().decode(errors="replace")
        raise ConnectionError(f"Scan processing failed ({exc.code}): {body}")
    except (urllib.error.URLError, TimeoutError, OSError) as exc:
        raise ConnectionError(f"Could not reach Oncecheck server: {exc}")
