"""
xmldev.cli
~~~~~~~~~~
Command-line interface for xmldev.

Commands:
    validate  Validate XML against schema and write audit.
    repair    Repair XML and optionally auto-apply high-confidence patches.
    lint      Lint a file or directory and write a report.
    serve     Start an HTTP server for remote pipeline access.

Exit codes:
    0  Success and passes schema.
    1  Validation failed and no fixes applied.
    2  Fixed but needs human review (low-confidence patches present).
    3  Fatal error (invalid schema, I/O error).
"""
from __future__ import annotations

import json
import logging
import sys
from pathlib import Path

import click

from . import Xmldev
from .audit import AuditWriter
from .schema import SchemaLoadError

logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s")


def _exit_code(result: dict) -> int:
    """Determine the CLI exit code from a validate_and_fix result dict."""
    if not result["ok"]:
        return 1
    patches = result.get("patches", [])
    has_low_confidence = any(p.get("confidence", 1.0) < 0.9 for p in patches)
    if has_low_confidence:
        return 2
    return 0


@click.group()
@click.version_option(package_name="xmldev")
def main() -> None:
    """xmldev — XML validation, repair, and normalization."""


# ---------------------------------------------------------------------------
# validate
# ---------------------------------------------------------------------------

@main.command("validate")
@click.option("--schema", "schema_path", required=True, type=click.Path(exists=True), help="Path to ground-truth JSON schema.")
@click.option("--input", "input_path", required=True, type=click.Path(exists=True), help="Input XML file.")
@click.option("--output", "output_path", default=None, type=click.Path(), help="Write fixed XML to this path.")
@click.option("--audit", "audit_path", default=None, type=click.Path(), help="Write audit JSON to this path.")
@click.option("--fuzzy-profile", default="balanced", show_default=True, type=click.Choice(["strict", "balanced", "permissive"]))
@click.option("--allow-llm", is_flag=True, default=False, help="Enable LLM fallback if configured.")
@click.option("--config", "config_path", default=None, type=click.Path(), help="Path to xmldev.config.env.")
@click.option("--verbose", "-v", is_flag=True)
def validate_cmd(
    schema_path: str,
    input_path: str,
    output_path: str | None,
    audit_path: str | None,
    fuzzy_profile: str,
    allow_llm: bool,
    config_path: str | None,
    verbose: bool,
) -> None:
    """Validate and repair an XML file against a ground-truth schema."""
    if verbose:
        logging.getLogger("xmldev").setLevel(logging.DEBUG)

    try:
        xd = Xmldev(config_path=config_path, fuzzy_profile=fuzzy_profile)
        schema = xd.load_schema_from_file(schema_path)
    except (SchemaLoadError, OSError) as exc:
        click.echo(f"[XM01/XM03] Fatal: {exc}", err=True)
        sys.exit(3)

    try:
        xml_bytes = Path(input_path).read_bytes()
    except OSError as exc:
        click.echo(f"[XM03] Cannot read input: {exc}", err=True)
        sys.exit(3)

    result = xd.validate_and_fix(xml_bytes, schema, allow_llm=allow_llm)

    _print_result_summary(result, verbose)

    if output_path and result["fixed_xml"]:
        Path(output_path).write_text(result["fixed_xml"], encoding="utf-8")
        click.echo(f"Fixed XML written to: {output_path}")

    if audit_path:
        _write_audit(result, audit_path)
        click.echo(f"Audit written to: {audit_path}")

    code = _exit_code(result)
    if code == 1:
        click.echo("Validation failed. No fixes applied.", err=True)
    elif code == 2:
        click.echo("Fixed, but some patches need human review (low confidence).")
    sys.exit(code)


# ---------------------------------------------------------------------------
# repair
# ---------------------------------------------------------------------------

@main.command("repair")
@click.option("--schema", "schema_path", required=True, type=click.Path(exists=True))
@click.option("--input", "input_path", required=True, type=click.Path(exists=True))
@click.option("--output", "output_path", required=True, type=click.Path())
@click.option("--auto-apply", is_flag=True, default=False, help="Auto-apply patches with confidence >= threshold.")
@click.option("--auto-apply-threshold", default=0.9, show_default=True, type=float)
@click.option("--aggressive", is_flag=True, default=False, help="Enable heuristic repair pass.")
@click.option("--fuzzy-profile", default="balanced", show_default=True, type=click.Choice(["strict", "balanced", "permissive"]))
@click.option("--allow-llm", is_flag=True, default=False)
@click.option("--config", "config_path", default=None, type=click.Path())
@click.option("--audit", "audit_path", default=None, type=click.Path())
@click.option("--verbose", "-v", is_flag=True)
def repair_cmd(
    schema_path: str,
    input_path: str,
    output_path: str,
    auto_apply: bool,
    auto_apply_threshold: float,
    aggressive: bool,
    fuzzy_profile: str,
    allow_llm: bool,
    config_path: str | None,
    audit_path: str | None,
    verbose: bool,
) -> None:
    """Repair an XML file and write the fixed output."""
    if verbose:
        logging.getLogger("xmldev").setLevel(logging.DEBUG)

    try:
        xd = Xmldev(config_path=config_path, fuzzy_profile=fuzzy_profile, aggressive=aggressive)
        schema = xd.load_schema_from_file(schema_path)
    except (SchemaLoadError, OSError) as exc:
        click.echo(f"Fatal: {exc}", err=True)
        sys.exit(3)

    try:
        xml_bytes = Path(input_path).read_bytes()
    except OSError as exc:
        click.echo(f"Cannot read input: {exc}", err=True)
        sys.exit(3)

    result = xd.validate_and_fix(xml_bytes, schema, allow_llm=allow_llm)

    if result["fixed_xml"]:
        fixed = result["fixed_xml"]
        if not auto_apply:
            # Only write patches that are auto_applied=True
            non_applied = [p for p in result["patches"] if not p.get("auto_applied")]
            if non_applied:
                click.echo(f"{len(non_applied)} patch(es) were NOT auto-applied (low confidence).")
                click.echo("Re-run with --auto-apply to apply all, or review the audit.")

        Path(output_path).write_text(fixed, encoding="utf-8")
        click.echo(f"Output written to: {output_path}")

    if audit_path:
        _write_audit(result, audit_path)

    _print_result_summary(result, verbose)
    sys.exit(_exit_code(result))


# ---------------------------------------------------------------------------
# lint
# ---------------------------------------------------------------------------

@main.command("lint")
@click.option("--schema", "schema_path", required=True, type=click.Path(exists=True))
@click.option("--input", "input_path", required=True, type=click.Path(exists=True))
@click.option("--report", "report_path", default=None, type=click.Path())
@click.option("--fuzzy-profile", default="balanced", show_default=True, type=click.Choice(["strict", "balanced", "permissive"]))
@click.option("--verbose", "-v", is_flag=True)
def lint_cmd(
    schema_path: str,
    input_path: str,
    report_path: str | None,
    fuzzy_profile: str,
    verbose: bool,
) -> None:
    """Lint an XML file or directory and report violations."""
    try:
        xd = Xmldev(fuzzy_profile=fuzzy_profile)
        schema = xd.load_schema_from_file(schema_path)
    except (SchemaLoadError, OSError) as exc:
        click.echo(f"Fatal: {exc}", err=True)
        sys.exit(3)

    target = Path(input_path)
    if target.is_dir():
        files = list(target.rglob("*.xml"))
    else:
        files = [target]

    all_results: list[dict] = []
    any_failed = False

    for f in files:
        try:
            xml_bytes = f.read_bytes()
        except OSError as exc:
            click.echo(f"Cannot read {f}: {exc}", err=True)
            continue
        result = xd.validate_and_fix(xml_bytes, schema)
        entry = {
            "file": str(f),
            "ok": result["ok"],
            "patches": len(result["patches"]),
            "diagnostics": result["diagnostics"],
        }
        all_results.append(entry)
        status = "✓" if result["ok"] else "✗"
        click.echo(f"  {status} {f}  ({len(result['patches'])} patch(es))")
        if not result["ok"]:
            any_failed = True
            for d in result["diagnostics"]:
                click.echo(f"      {d}", err=True)

    if report_path:
        Path(report_path).write_text(json.dumps(all_results, indent=2), encoding="utf-8")
        click.echo(f"\nReport written to: {report_path}")

    click.echo(f"\n{len(files)} file(s) linted.")
    sys.exit(1 if any_failed else 0)


# ---------------------------------------------------------------------------
# serve
# ---------------------------------------------------------------------------

@main.command("serve")
@click.option("--port", default=8080, show_default=True, type=int)
@click.option("--allow-llm", is_flag=True, default=False)
@click.option("--config", "config_path", default=None, type=click.Path())
@click.option("--verbose", "-v", is_flag=True)
def serve_cmd(port: int, allow_llm: bool, config_path: str | None, verbose: bool) -> None:
    """Start an HTTP server for remote XML repair pipelines."""
    try:
        import http.server
        import threading
        from urllib.parse import parse_qs
    except ImportError as exc:
        click.echo(f"Server mode unavailable: {exc}", err=True)
        sys.exit(3)

    if verbose:
        logging.getLogger("xmldev").setLevel(logging.DEBUG)

    # Lazy import prometheus (optional in serve mode)
    try:
        from prometheus_client import Counter, Histogram, start_http_server as prom_start
        METRICS_ENABLED = True
        requests_total = Counter("xmldev_requests_total", "Total requests")
        requests_success = Counter("xmldev_requests_success", "Successful repairs")
        requests_failure = Counter("xmldev_requests_failure", "Failed repairs")
        repair_time = Histogram("xmldev_repair_seconds", "Repair duration")
        prom_start(port + 1)
        click.echo(f"Prometheus metrics on :{port + 1}/metrics")
    except Exception:
        METRICS_ENABLED = False

    import time

    xd = Xmldev(config_path=config_path, aggressive=True)

    class Handler(http.server.BaseHTTPRequestHandler):
        def log_message(self, *args: object) -> None:
            if verbose:
                super().log_message(*args)

        def do_POST(self) -> None:  # noqa: N802
            if self.path != "/repair":
                self.send_response(404)
                self.end_headers()
                return

            length = int(self.headers.get("Content-Length", 0))
            body = self.rfile.read(length)

            try:
                payload = json.loads(body)
                schema_json = payload["schema"]
                xml_input = payload["xml"]
                schema = xd.load_schema_from_string(json.dumps(schema_json))
            except Exception as exc:
                self.send_response(400)
                self.end_headers()
                self.wfile.write(json.dumps({"error": str(exc)}).encode())
                if METRICS_ENABLED:
                    requests_total.inc(); requests_failure.inc()  # type: ignore[union-attr]
                return

            t0 = time.monotonic()
            result = xd.validate_and_fix(xml_input, schema, allow_llm=allow_llm)
            elapsed = time.monotonic() - t0

            if METRICS_ENABLED:
                requests_total.inc()  # type: ignore[union-attr]
                repair_time.observe(elapsed)  # type: ignore[union-attr]
                if result["ok"]:
                    requests_success.inc()  # type: ignore[union-attr]
                else:
                    requests_failure.inc()  # type: ignore[union-attr]

            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(result).encode())

    server = http.server.HTTPServer(("", port), Handler)
    click.echo(f"xmldev serving on http://0.0.0.0:{port}/repair  (Ctrl-C to stop)")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        click.echo("\nServer stopped.")


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _print_result_summary(result: dict, verbose: bool) -> None:
    patches = result.get("patches", [])
    ok = result["ok"]
    status = "✓ PASS" if ok else "✗ FAIL"
    click.echo(f"Status: {status}  |  Patches: {len(patches)}  |  Provenance: {result['provenance'].get('source')}")
    if verbose:
        for p in patches:
            marker = "✓" if p.get("auto_applied") else "○"
            click.echo(f"  {marker} [{p['type']:8s}] conf={p['confidence']:.2f}  {p['path']}  {p['notes']}")
    if result.get("diagnostics"):
        for d in result["diagnostics"]:
            click.echo(f"  DIAG: {d}", err=True)


def _write_audit(result: dict, audit_path: str) -> None:
    from .audit import AuditWriter, Patch
    patches = [Patch(**{k: v for k, v in p.items()}) for p in result.get("patches", [])]
    AuditWriter.write(patches, audit_path)
