"""
Per-service progress for scan_details_advance + incremental scan_details.result merge.
Used by scan.py (subprocess) and scan_queue_worker.py.
"""
from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from typing import Any

CATALOG_ORDER = ("httpx", "nmap", "zap", "nuclei", "subfinder", "nikto", "leakcheck")
IMPLEMENTED = frozenset({"httpx", "nmap", "zap", "nuclei"})
STUB_MSG = "Tool not yet implemented in scan pipeline"


def utc_now() -> str:
    return datetime.now(timezone.utc).isoformat()


def get_client():
    from supabase import create_client

    url = (os.environ.get("SUPABASE_URL") or "").strip()
    key = (os.environ.get("SUPABASE_SERVICE_ROLE_KEY") or "").strip()
    if not url or not key:
        return None
    return create_client(url, key)


def load_advance_config(client, scan_id: str) -> dict | None:
    if not client or not scan_id:
        return None
    r = (
        client.table("scan_details_advance")
        .select("scan_mode,services,options")
        .eq("scan_id", scan_id)
        .limit(1)
        .execute()
    )
    rows = r.data or []
    return rows[0] if rows else None


def enabled_tools_from_services(services: dict) -> list[str]:
    tools: list[str] = []
    for name in CATALOG_ORDER:
        svc = services.get(name)
        if isinstance(svc, dict) and svc.get("enabled"):
            tools.append(name)
    return tools


def build_summary(findings: list[dict]) -> dict[str, int]:
    summary = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
    for f in findings:
        sev = str(f.get("severity") or "info").lower()
        if sev in summary:
            summary[sev] += 1
        else:
            summary["info"] += 1
    return summary


def build_report_summary(findings: list[dict]) -> dict[str, Any]:
    summary = build_summary(findings)
    total = len(findings)
    return {
        **summary,
        "total_findings": total,
        "total": total,
        "critical_high": summary["critical"] + summary["high"],
    }


def option_timeout_sec(tool: str, services: dict, ceiling: int, default: int) -> int:
    svc = services.get(tool) or {}
    opts = svc.get("options") or {}
    if tool == "nikto":
        bag = opts.get("maxtime_sec") or {}
        if isinstance(bag, dict) and bag.get("enabled", True):
            try:
                return min(int(bag.get("value", default)), ceiling)
            except (TypeError, ValueError):
                pass
    if tool == "zap":
        bag = opts.get("max_duration_min") or {}
        if isinstance(bag, dict) and bag.get("enabled", True):
            try:
                return min(int(bag.get("value", default)) * 60, ceiling)
            except (TypeError, ValueError):
                pass
    if tool == "subfinder":
        bag = opts.get("max_time_min") or {}
        if isinstance(bag, dict) and bag.get("enabled", True):
            try:
                return min(int(bag.get("value", default)) * 60, ceiling)
            except (TypeError, ValueError):
                pass
    return min(default, ceiling)


def _fetch_services(client, scan_id: str) -> dict:
    row = load_advance_config(client, scan_id)
    if not row:
        return {}
    services = row.get("services") or {}
    return services if isinstance(services, dict) else {}


def _patch_services(client, scan_id: str, services: dict) -> None:
    client.table("scan_details_advance").update(
        {"services": services, "updated_at": utc_now()}
    ).eq("scan_id", scan_id).execute()


def update_service(
    client,
    scan_id: str,
    service_id: str,
    status: str,
    *,
    result: Any = None,
    findings_count: int | None = None,
    error: str | None = None,
) -> None:
    if not client or not scan_id:
        return
    services = _fetch_services(client, scan_id)
    svc = services.get(service_id)
    if not isinstance(svc, dict):
        svc = {"enabled": True, "options": {}}
    svc["status"] = status
    if status == "started":
        svc["started_at"] = utc_now()
    if status in ("completed", "failed"):
        svc["completed_at"] = utc_now()
    if findings_count is not None:
        svc["findings_count"] = findings_count
    if result is not None:
        svc["result"] = result
    elif error:
        svc["result"] = {"error": error}
    services[service_id] = svc
    _patch_services(client, scan_id, services)


def merge_scan_result(
    client,
    scan_id: str,
    findings: list[dict],
    *,
    target: str,
    target_name: str | None = None,
    warnings: list[str] | None = None,
) -> dict:
    """Merge findings into scan_details.result and return full result dict."""
    existing: dict = {}
    if client and scan_id:
        r = client.table("scan_details").select("result").eq("id", scan_id).limit(1).execute()
        rows = r.data or []
        if rows and isinstance(rows[0].get("result"), dict):
            existing = rows[0]["result"]
        elif rows and isinstance(rows[0].get("result"), str):
            try:
                existing = json.loads(rows[0]["result"])
            except json.JSONDecodeError:
                existing = {}

    merged_findings = list(existing.get("findings") or [])
    seen = {json.dumps(f, sort_keys=True, default=str) for f in merged_findings}
    for f in findings:
        key = json.dumps(f, sort_keys=True, default=str)
        if key not in seen:
            merged_findings.append(f)
            seen.add(key)

    result = {
        "target": target,
        "scan_date": utc_now(),
        "total_findings": len(merged_findings),
        "summary": build_summary(merged_findings),
        "findings": merged_findings,
    }
    if target_name:
        result["target_name"] = target_name
    if warnings:
        result["warnings"] = warnings
    if existing.get("id"):
        result["id"] = existing["id"]

    if client and scan_id:
        client.table("scan_details").update(
            {
                "result": result,
                "report_summary": build_report_summary(merged_findings),
                "updated_at": utc_now(),
            }
        ).eq("id", scan_id).execute()
    return result


class ScanProgress:
    """Hook scan.py tool lifecycle to scan_details_advance + scan_details.result."""

    def __init__(self, scan_id: str | None, advance_config: dict | None = None):
        self.scan_id = (scan_id or "").strip() or None
        self.client = get_client() if self.scan_id else None
        self.advance_config = advance_config or {}
        self.services: dict = self.advance_config.get("services") or {}
        if self.scan_id and not self.services and self.client:
            row = load_advance_config(self.client, self.scan_id)
            if row:
                self.advance_config = row
                self.services = row.get("services") or {}

    def has_advance(self) -> bool:
        return bool(self.scan_id and self.services and enabled_tools_from_services(self.services))

    def enabled_tools(self) -> list[str] | None:
        if not self.has_advance():
            return None
        return enabled_tools_from_services(self.services)

    def tool_timeout(self, tool: str, ceiling: int, default: int) -> int:
        if self.services:
            return option_timeout_sec(tool, self.services, ceiling, default)
        return min(default, ceiling)

    def on_start(self, tool: str) -> None:
        if self.client and self.scan_id:
            update_service(self.client, self.scan_id, tool, "started")

    def on_complete(
        self,
        tool: str,
        findings: list[dict],
        *,
        target: str,
        target_name: str | None = None,
        warnings: list[str] | None = None,
        error: str | None = None,
    ) -> None:
        if self.client and self.scan_id:
            status = "failed" if error else "completed"
            slice_result = {"findings": findings, "count": len(findings)}
            if error:
                slice_result["error"] = error
            update_service(
                self.client,
                self.scan_id,
                tool,
                status,
                result=slice_result,
                findings_count=len(findings),
                error=error,
            )
            merge_scan_result(
                self.client,
                self.scan_id,
                findings,
                target=target,
                target_name=target_name,
                warnings=warnings,
            )

    def mark_unimplemented(self, tool: str) -> None:
        if self.client and self.scan_id:
            update_service(
                self.client,
                self.scan_id,
                tool,
                "failed",
                result={"error": STUB_MSG},
                findings_count=0,
                error=STUB_MSG,
            )
        print(f"[!] {tool}: {STUB_MSG}", flush=True)
