from __future__ import annotations

import json
import webbrowser
from pathlib import Path
from typing import Any

from auditwalk.compare.differ import default_diff_path, diff
from auditwalk.core.errors import EXIT_COMPARE_FAILED, EXIT_OK, EXIT_REPORT_FAILED, EXIT_SCAN_FAILED
from auditwalk.core.jsonio import write_json
from auditwalk.report.render_html import render_html
from auditwalk.runs.run_store import RunStore
from auditwalk.scan.scanner import scan
from auditwalk.score.scoring import score


def cmd_scan(args: Any) -> int:
    try:
        result = scan(
            mode=args.mode,
            roots=args.roots,
            exclude=args.exclude,
            label=args.label,
            baseline=args.baseline,
            do_hash=args.hash,
            max_files=args.max_files,
            max_procs=args.max_procs,
            timeout_seconds=args.timeout_seconds,
            strict_files=args.strict_files,
            data_dir=args.data_dir,
        )
    except Exception as exc:
        print(f"scan failed: {exc}")
        return EXIT_SCAN_FAILED

    store = RunStore(args.data_dir)
    data_dir = Path(store.data_dir)
    try:
        store.apply_default_compare_retention()
        store.prune_orphan_compares()
    except Exception:
        pass

    run_bundle = store.load_run_bundle(result["run_id"])
    rules = Path(Path(__file__).resolve().parents[1] / "data" / "scoring_rules.yml")

    # Always score the run (run-only context), then optionally rescore with diff context.
    run_score = score(run_bundle, None, rules_path=rules)
    store.save_score(result["run_id"], run_score)
    run_bundle["score"] = run_score
    result["score"] = run_score
    result["top_findings"] = run_score.get("findings", [])[:3]

    diff_bundle = None
    compare_target = args.compare
    if compare_target == "baseline":
        compare_target = getattr(args, "baseline_run_id", None)
        if not compare_target:
            print("warning: compare target 'baseline' requested, but baseline_run_id is not configured; compare skipped.")

    if args.compare_last:
        refs = store.list_runs(last=2)
        if len(refs) >= 2:
            compare_target = refs[1].run_id
        else:
            baseline_target = getattr(args, "baseline_run_id", None)
            if baseline_target:
                compare_target = baseline_target
                print(f"note: --compare-last fallback to configured baseline run: {baseline_target}")
            else:
                print("warning: --compare-last requested, but no prior run exists; compare skipped.")

    if compare_target:
        try:
            left = store.load_run_bundle(compare_target)
            diff_bundle = diff(left, run_bundle)
            out_path = default_diff_path(data_dir, compare_target, result["run_id"])
            write_json(out_path, diff_bundle)
            store.sync_compare_index()

            # Rescore run with diff context.
            run_score = score(run_bundle, diff_bundle, rules_path=rules)
            store.save_score(result["run_id"], run_score)
            run_bundle["score"] = run_score
            result["score"] = run_score
            result["top_findings"] = run_score.get("findings", [])[:3]
        except Exception as exc:
            print(f"compare failed: {exc}")
            return EXIT_COMPARE_FAILED

    if args.html:
        try:
            html_out = Path(args.html_out) if args.html_out else data_dir / "reports" / f"{result['run_id']}.html"
            html_out.parent.mkdir(parents=True, exist_ok=True)
            html_out.write_text(render_html(run_bundle, diff_bundle=diff_bundle), encoding="utf-8")
            if args.open:
                webbrowser.open(html_out.resolve().as_uri())
            result["html_path"] = str(html_out)
        except Exception as exc:
            print(f"report failed: {exc}")
            return EXIT_REPORT_FAILED

    _print_scan_summary(result)
    _print_scan_warnings(run_bundle)
    if args.json:
        print(json.dumps(result, ensure_ascii=True))
    return EXIT_OK


def _print_scan_summary(result: dict) -> None:
    score = result.get("score", {}) if isinstance(result.get("score"), dict) else {}
    total = score.get("total", 0)
    tier = score.get("tier", "Clean")
    findings = result.get("top_findings", []) if isinstance(result.get("top_findings", []), list) else []

    print("AuditWalk v0.1")
    print(f"Mode: {str(result.get('mode', 'unknown')).upper()}")
    print(f"Run: {result.get('run_id')}")
    if result.get("label"):
        print(f"Label: {result.get('label')}")
    print()
    print(f"Score: {total}  ({str(tier).upper()})")
    print()
    print("Findings:")
    if findings:
        for f in findings[:3]:
            pts = f.get("points", 0)
            rid = f.get("rule_id", "RULE")
            msg = f.get("message", "finding")
            print(f"  +{pts:<3} {rid:<12} {msg}")
    else:
        print("  (none)")
    print()
    print("Artifacts:")
    print(f"  {result.get('run_path')}")
    if result.get("html_path"):
        print(f"  {result.get('html_path')}")


def _print_scan_warnings(run_bundle: dict) -> None:
    meta = run_bundle.get("meta", {}) if isinstance(run_bundle.get("meta"), dict) else {}
    collector_status = meta.get("collector_status", {}) if isinstance(meta.get("collector_status"), dict) else {}
    partial_collectors = [name for name, state in collector_status.items() if state in {"partial", "error"}]
    files_data = run_bundle.get("files", {}) if isinstance(run_bundle.get("files"), dict) else {}
    summary = files_data.get("collector_summary", {}) if isinstance(files_data.get("collector_summary"), dict) else {}
    vanished = int(summary.get("vanished", 0)) if isinstance(summary.get("vanished"), int) else 0

    if vanished > 0 and "files" not in partial_collectors:
        print(f"Note: {vanished} files changed during scan (vanished).")

    if not partial_collectors:
        return
    joined = ", ".join(sorted(partial_collectors))
    print(f"Warning: partial collector results detected: {joined}")
