"""Transfer-risk assessment orchestration module.

Detector and scoring details live in feature modules:
- `auditwalk.detectors.*`
- `auditwalk.risk_decision`
"""

from __future__ import annotations

import json
import shutil
import time
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

from .detectors import add_environment_signals, run_filesystem_detectors
from .risk.reason_codes import (
    POLICY_HIGH_RISK_BLOCK_ENFORCED,
    POLICY_MEDIUM_RISK_WARNING_REQUIRED,
)
from .risk.risk_decision import policy_decision, score_from_signals
from .risk.risk_policy import load_risk_policy
from .threat_intel import load_threat_hashes


ENGINE_VERSION = "0.1.0"


@dataclass(slots=True)
class AssessTransferConfig:
    request_id: Optional[str]
    asset: str
    chain: str
    amount: str
    wallet_app: str
    wallet_profile: str
    wallet_paths: list[str]
    process_names: list[str]
    scan_mode: str
    scan_path: Path
    baseline_path: Path
    threat_db_path: Path
    risk_policy_path: Path
    recent_hours: int
    policy_on_high: str
    policy_on_medium: str
    allow_override: bool
    max_scan_seconds: int
    include_reports: bool
    json_out: Optional[Path]
    tech_report_out: Optional[Path]
    desktop_copy_report: bool


def _now_iso() -> str:
    return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())


def _load_baseline(path: Path) -> dict[str, dict]:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return {}
    if not isinstance(data, dict):
        return {}
    files = data.get("files", {})
    return files if isinstance(files, dict) else {}


def _build_tech_report(payload: dict) -> str:
    risk = payload["risk"]
    decision = payload["decision"]
    request = payload["request"]
    scan_summary = payload["scan_summary"]
    engine = payload["engine"]

    lines = [
        "AuditWalk Technical Report",
        f"Generated: {payload['generated_at']}",
        f"Request ID: {payload['request_id']}",
        f"Assessment ID: {payload['assessment_id']}",
        "",
        "Request",
        f"- Asset: {request['asset']}",
        f"- Chain: {request['chain']}",
        f"- Amount: {request['amount']}",
        f"- Wallet App: {request['wallet_context']['wallet_app'] or 'n/a'}",
        f"- Wallet Profile: {request['wallet_context']['wallet_profile'] or 'n/a'}",
        "",
        "Risk Summary",
        f"- Level: {risk['level']}",
        f"- Score: {risk['score']}",
        f"- Thresholds: medium>={risk['thresholds']['medium_min']} high>={risk['thresholds']['high_min']}",
        f"- Top Reasons: {', '.join(risk['top_reasons']) if risk['top_reasons'] else 'none'}",
        "",
        "Decision",
        f"- Action: {decision['action']}",
        f"- Requires User Ack: {decision['requires_user_ack']}",
        f"- Override Allowed: {decision['override_allowed']}",
        f"- Override TTL Sec: {decision['override_token_ttl_sec']}",
        "",
        "Scan Summary",
        f"- Mode: {scan_summary['mode']}",
        f"- Duration Ms: {scan_summary['duration_ms']}",
        f"- Files Scanned: {scan_summary['files_scanned']}",
        f"- Files Hashed: {scan_summary['files_hashed']}",
        f"- Baseline Used: {scan_summary['baseline_used']}",
        f"- Baseline Version: {scan_summary['baseline_version'] or 'n/a'}",
        "",
        "Engine",
        f"- Version: {engine['version']}",
        f"- Threat DB Version: {engine['threat_db_version']}",
        f"- Rule Bundle Version: {engine['rule_bundle_version']}",
        f"- Risk Policy Version: {engine['risk_policy_version']}",
        "",
        "Signals",
    ]

    for signal in payload["signals"]:
        lines.append(
            (
                f"- {signal['reason_code']} | severity={signal['severity']} "
                f"confidence={signal['confidence']} weight={signal['weight']}"
            )
        )
    if not payload["signals"]:
        lines.append("- none")

    return "\n".join(lines) + "\n"


def _resolve_report_path(base: Optional[Path]) -> Path:
    filename = "technical_report.txt"
    if base is None:
        return Path("reports") / filename

    if base.suffix.lower() == ".txt":
        return base
    return base / filename


def assess_transfer(config: AssessTransferConfig) -> tuple[dict, int]:
    """Run transfer-time risk assessment.

    Returns `(payload, exit_code)` where exit codes are:
    - 0: allow/warn decision
    - 10: block decision
    - 20: runtime/config error
    """
    request_id = config.request_id or str(uuid.uuid4())
    assessment_id = str(uuid.uuid4())
    started = time.time()

    try:
        if not config.scan_path.exists():
            raise ValueError(f"scan_path does not exist: {config.scan_path}")

        # Feature: policy loading/configuration.
        risk_policy = load_risk_policy(config.risk_policy_path)
        thresholds = risk_policy["thresholds"]
        reason_config = risk_policy["reason_config"]
        suspicious_extensions = set(risk_policy["suspicious_extensions"])
        sensitive_path_tokens = set(risk_policy["sensitive_path_tokens"])

        # Feature: baseline and threat-intel inputs.
        recent_cutoff = time.time() - (config.recent_hours * 3600)
        baseline = _load_baseline(config.baseline_path)
        known_bad_hashes, feeds_meta = load_threat_hashes(config.threat_db_path)
        feed_generated_at = float(feeds_meta.get("generated_at", 0) or 0)
        threat_db_version = str(feeds_meta.get("version", "unknown"))
        rule_bundle_version = str(feeds_meta.get("rule_bundle_version", threat_db_version))

        # Feature: scan budgeting controls.
        max_scan_seconds = config.max_scan_seconds
        if config.scan_mode == "full":
            max_scan_seconds = max(max_scan_seconds, 20)
        max_files = 25000 if config.scan_mode == "quick" else 150000

        # Merge caller-provided wallet paths into sensitive tokens from policy.
        wallet_tokens = set(sensitive_path_tokens)
        for p in config.wallet_paths:
            wallet_tokens.add(p.lower())

        signals: list[dict] = []
        seen_reasons: set[str] = set()

        # Feature: normalized signal registration.
        def add_signal(reason_code: str, confidence: float, evidence: dict) -> None:
            if reason_code in seen_reasons:
                return
            cfg = reason_config[reason_code]
            signals.append(
                {
                    "reason_code": reason_code,
                    "severity": cfg["severity"],
                    "confidence": round(confidence, 2),
                    "weight": cfg["weight"],
                    "title": cfg["title"],
                    "evidence": evidence,
                }
            )
            seen_reasons.add(reason_code)

        # Feature: environment signals (baseline and intel health).
        add_environment_signals(
            baseline=baseline,
            baseline_path=config.baseline_path,
            threat_db_path=config.threat_db_path,
            feed_generated_at=feed_generated_at,
            add_signal=add_signal,
        )

        # Feature: filesystem detectors (heuristics + known IOC hash checks).
        scan_result = run_filesystem_detectors(
            scan_path=config.scan_path,
            started_at=started,
            max_scan_seconds=max_scan_seconds,
            max_files=max_files,
            recent_cutoff=recent_cutoff,
            suspicious_extensions=suspicious_extensions,
            wallet_tokens=wallet_tokens,
            baseline=baseline,
            known_bad_hashes=known_bad_hashes,
            add_signal=add_signal,
        )

        # Feature: scoring and policy decision.
        score, level = score_from_signals(signals, thresholds)
        top_reasons = [s["reason_code"] for s in sorted(signals, key=lambda s: s["weight"], reverse=True)[:3]]
        decision = policy_decision(level, config.policy_on_high, config.policy_on_medium, config.allow_override)

        if decision["action"] == "block":
            signals.append(
                {
                    "reason_code": POLICY_HIGH_RISK_BLOCK_ENFORCED,
                    "severity": "high",
                    "confidence": 1.0,
                    "weight": 0,
                    "title": "Policy enforced high-risk block",
                    "evidence": {"policy_on_high": config.policy_on_high},
                }
            )
        elif decision["action"] == "warn":
            signals.append(
                {
                    "reason_code": POLICY_MEDIUM_RISK_WARNING_REQUIRED,
                    "severity": "medium",
                    "confidence": 1.0,
                    "weight": 0,
                    "title": "Policy requires warning",
                    "evidence": {"policy_on_medium": config.policy_on_medium},
                }
            )

        payload = {
            "request_id": request_id,
            "assessment_id": assessment_id,
            "generated_at": _now_iso(),
            "request": {
                "asset": config.asset,
                "chain": config.chain,
                "amount": config.amount,
                "wallet_context": {
                    "wallet_app": config.wallet_app,
                    "wallet_profile": config.wallet_profile,
                    "wallet_paths": config.wallet_paths,
                    "process_names": config.process_names,
                },
            },
            "risk": {
                "level": level,
                "score": score,
                "thresholds": thresholds,
                "top_reasons": top_reasons,
            },
            "decision": decision,
            "signals": signals,
            "reports": {
                "json_report_path": str(config.json_out) if config.json_out and config.include_reports else None,
                "tech_report_path": None,
                "desktop_tech_report_path": None,
            },
            "scan_summary": {
                "mode": config.scan_mode,
                "duration_ms": int((time.time() - started) * 1000),
                "files_scanned": scan_result.files_scanned,
                "files_hashed": scan_result.files_hashed,
                "baseline_used": bool(baseline),
                "baseline_version": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(config.baseline_path.stat().st_mtime))
                if config.baseline_path.exists()
                else None,
            },
            "engine": {
                "version": ENGINE_VERSION,
                "threat_db_version": threat_db_version,
                "rule_bundle_version": rule_bundle_version,
                "risk_policy_version": risk_policy.get("version", "unknown"),
            },
        }

        if config.include_reports:
            tech_report_path = _resolve_report_path(config.tech_report_out).expanduser()
            tech_report_path.parent.mkdir(parents=True, exist_ok=True)
            tech_report_text = _build_tech_report(payload)
            tech_report_path.write_text(tech_report_text, encoding="utf-8")
            payload["reports"]["tech_report_path"] = str(tech_report_path)

            if config.desktop_copy_report:
                desktop_dir = Path.home() / "Desktop" / "auditwalk-reports"
                desktop_dir.mkdir(parents=True, exist_ok=True)
                desktop_path = desktop_dir / tech_report_path.name
                shutil.copy2(tech_report_path, desktop_path)
                payload["reports"]["desktop_tech_report_path"] = str(desktop_path)

        if decision["action"] == "block":
            return payload, 10
        return payload, 0
    except Exception as exc:
        return {
            "request_id": request_id,
            "generated_at": _now_iso(),
            "error": {
                "code": "ENGINE_UNAVAILABLE",
                "message": str(exc),
                "retryable": False,
            },
        }, 20
