from __future__ import annotations

import argparse
import json
import os
import sys
import uuid
from pathlib import Path
from typing import Any
from urllib import error, request


def _import_dotenv(path: Path) -> None:
    if not path.exists():
        return
    for raw in path.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        if line.lower().startswith("export "):
            line = line[7:].strip()
        name, value = line.split("=", 1)
        value = value.strip()
        if len(value) >= 2 and ((value[0] == value[-1] == '"') or (value[0] == value[-1] == "'")):
            value = value[1:-1]
        elif " #" in value:
            value = value.split(" #", 1)[0].strip()
        os.environ.setdefault(name.strip(), value)


def _parse_json(raw: str) -> Any:
    if not raw:
        return None
    try:
        return json.loads(raw)
    except json.JSONDecodeError:
        return {"raw": raw}


def _json_request(
    method: str,
    url: str,
    token: str,
    *,
    tenant_id: str = "",
    body: Any | None = None,
    timeout_s: float = 40.0,
) -> tuple[int, Any]:
    payload = None
    headers = {"Authorization": f"Bearer {token}"}
    if tenant_id:
        headers["X-Kyb-Tenant-Id"] = tenant_id
    if body is not None:
        payload = json.dumps(body).encode("utf-8")
        headers["Content-Type"] = "application/json"
    req = request.Request(url, data=payload, headers=headers, method=method)
    try:
        with request.urlopen(req, timeout=timeout_s) as resp:
            return resp.status, _parse_json(resp.read().decode("utf-8"))
    except error.HTTPError as exc:
        return exc.code, _parse_json(exc.read().decode("utf-8"))


def _unwrap_data(status: int, payload: Any, label: str) -> Any:
    if status != 200:
        raise RuntimeError(f"{label}:{status}:{payload}")
    if isinstance(payload, dict) and "data" in payload:
        return payload["data"]
    return payload


def _load_json_arg(raw: str) -> dict[str, Any]:
    text = raw.strip()
    if not text:
        return {}
    parsed = json.loads(text)
    if not isinstance(parsed, dict):
        raise ValueError("expected JSON object")
    return parsed


def _load_json_file_or_arg(raw: str) -> dict[str, Any]:
    text = raw.strip()
    if not text:
        return {}
    path = Path(text)
    if path.exists():
        parsed = json.loads(path.read_text(encoding="utf-8"))
        if not isinstance(parsed, dict):
            raise ValueError(f"{text}: expected JSON object")
        return parsed
    return _load_json_arg(text)


def _new_intent(token: str, recipe: str) -> dict[str, Any]:
    return {
        "tool_name": f"kybernis_audit_{recipe}_write",
        "action": "execute",
        "policy_version": "audit_cli:v1",
        "dependency_signature": "sha256:audit-cli",
        "context_ordering_signature": "audit->intent->provider",
        "identity_fields": {
            "operation": "external_write",
            "target": f"audit_cli_{recipe}",
            "token": token,
        },
        "field_semantic_hashes": {
            "operation": "sha256:external_write",
            "target": f"sha256:audit_cli_{recipe}",
            "token": f"sha256:{token}",
        },
        "payload": {
            "object_ref": f"audit-cli://{recipe}/{token}",
            "amount": 1,
            "currency": "USD",
        },
    }


def _create_run(api_base: str, tenant_id: str, api_key: str, input_context_ref: str) -> str:
    status, payload = _json_request(
        "POST",
        api_base.rstrip("/") + "/infra/runs/get_or_create",
        api_key,
        tenant_id=tenant_id,
        body={"tenant_id": tenant_id, "input_context_ref": input_context_ref},
    )
    data = _unwrap_data(status, payload, "run_create_failed")
    run_id = str(data.get("run_id") or "")
    if not run_id:
        raise RuntimeError("run_create_missing_run_id")
    return run_id


def _guard(
    api_base: str,
    tenant_id: str,
    api_key: str,
    run_id: str,
    idempotency_key: str,
    intent: dict[str, Any],
    *,
    requested_mode: str = "warn_only",
    original_payload: dict[str, Any] | None = None,
) -> dict[str, Any]:
    status, payload = _json_request(
        "POST",
        api_base.rstrip("/") + "/infra/intent/guard",
        api_key,
        tenant_id=tenant_id,
        body={
            "tenant_id": tenant_id,
            "run_id": run_id,
            "idempotency_key": idempotency_key,
            "intent": intent,
            "requested_mode": requested_mode,
            "original_payload": original_payload or {},
        },
    )
    return _unwrap_data(status, payload, "intent_guard_failed")


def _resolve_mutation(
    api_base: str,
    tenant_id: str,
    api_key: str,
    mutation_id: str,
    runtime_intent_hash: str,
    *,
    provider: str,
    token: str,
    outcome: str,
) -> None:
    if outcome == "committed":
        status, payload = _json_request(
            "POST",
            api_base.rstrip("/") + "/infra/intent/commit",
            api_key,
            tenant_id=tenant_id,
            body={
                "tenant_id": tenant_id,
                "mutation_id": mutation_id,
                "runtime_intent_hash": runtime_intent_hash,
                "provider": provider,
                "receipt": {"provider_ref": f"audit-cli-{token}", "status": "ok"},
            },
        )
        _unwrap_data(status, payload, "intent_commit_failed")
        return
    if outcome == "unknown":
        status, payload = _json_request(
            "POST",
            api_base.rstrip("/") + "/infra/intent/unknown",
            api_key,
            tenant_id=tenant_id,
            body={"tenant_id": tenant_id, "mutation_id": mutation_id, "reason": "audit_cli_unknown"},
        )
        _unwrap_data(status, payload, "intent_unknown_failed")
        return
    if outcome == "failed":
        status, payload = _json_request(
            "POST",
            api_base.rstrip("/") + "/infra/intent/fail",
            api_key,
            tenant_id=tenant_id,
            body={"tenant_id": tenant_id, "mutation_id": mutation_id, "reason": "audit_cli_failed"},
        )
        _unwrap_data(status, payload, "intent_fail_failed")
        return
    raise ValueError(f"unsupported outcome: {outcome}")


def _create_compensation(api_base: str, tenant_id: str, api_key: str, mutation_id: str) -> str:
    status, payload = _json_request(
        "POST",
        api_base.rstrip("/") + "/infra/intent/compensation",
        api_key,
        tenant_id=tenant_id,
        body={
            "tenant_id": tenant_id,
            "mutation_id": mutation_id,
            "status": "REQUESTED",
            "reason": "audit_cli_saga_compensation",
            "plan": {"step": "reverse_external_effect", "recipe": "SAGA"},
            "result": {"state": "queued", "operator": "kybernis-audit"},
        },
    )
    data = _unwrap_data(status, payload, "compensation_failed")
    compensation_id = str(data.get("compensation_id") or "")
    if not compensation_id:
        raise RuntimeError("compensation_missing_id")
    return compensation_id


def _set_approval_policy(api_base: str, tenant_id: str, api_key: str, tool_name: str) -> None:
    status, payload = _json_request(
        "POST",
        api_base.rstrip("/") + "/infra/policies/approval",
        api_key,
        tenant_id=tenant_id,
        body={
            "tenant_id": tenant_id,
            "policy": {"rules": [{"tool_name": tool_name, "action": "execute", "require_gate": True}]},
        },
    )
    _unwrap_data(status, payload, "approval_policy_failed")


def _approve_gate(api_base: str, tenant_id: str, api_key: str, gate_id: str) -> None:
    status, payload = _json_request(
        "POST",
        api_base.rstrip("/") + "/infra/gate/decide",
        api_key,
        tenant_id=tenant_id,
        body={"gate_id": gate_id, "status": "APPROVED", "decided_by": "kybernis-audit", "reason": "quickstart"},
    )
    _unwrap_data(status, payload, "gate_decide_failed")


def _create_audit_run(
    api_base: str,
    tenant_id: str,
    api_key: str,
    *,
    template_id: str,
    target_adapter: str,
    source_run_id: str,
    mitigation_profile: dict[str, Any],
    failure_profile: dict[str, Any],
    metadata: dict[str, Any],
) -> dict[str, Any]:
    status, payload = _json_request(
        "POST",
        api_base.rstrip("/") + "/infra/audit/run",
        api_key,
        tenant_id=tenant_id,
        body={
            "tenant_id": tenant_id,
            "template_id": template_id,
            "target_adapter": target_adapter,
            "source_run_id": source_run_id,
            "mitigation_profile": mitigation_profile,
            "failure_profile": failure_profile,
            "metadata": metadata,
        },
        timeout_s=60.0,
    )
    return _unwrap_data(status, payload, "audit_run_failed")


def _fetch_report(api_base: str, tenant_id: str, api_key: str, audit_run_id: str) -> dict[str, Any]:
    status, payload = _json_request(
        "GET",
        api_base.rstrip("/") + f"/infra/audit/run/{audit_run_id}/report?tenant_id={tenant_id}",
        api_key,
        tenant_id=tenant_id,
        timeout_s=60.0,
    )
    data = _unwrap_data(status, payload, "audit_report_failed")
    if not isinstance(data, dict):
        raise RuntimeError("audit_report_invalid")
    return data


def _resolve_config(args: argparse.Namespace) -> tuple[str, str, str]:
    api_base = (args.api_base or os.environ.get("KYB_API_BASE") or "http://127.0.0.1:8080").strip()
    api_key = (args.api_key or os.environ.get("KYB_API_KEY") or os.environ.get("KYB_API_TOKEN") or "").strip()
    tenant_id = (args.tenant_id or os.environ.get("KYB_TENANT_ID") or os.environ.get("DEMO_TENANT_ID") or "").strip()
    if not api_key:
        raise SystemExit("missing API key: set KYB_API_KEY or pass --api-key")
    if not tenant_id:
        raise SystemExit("missing tenant id: set KYB_TENANT_ID or pass --tenant-id")
    return api_base.rstrip("/"), api_key, tenant_id


def _cmd_templates(args: argparse.Namespace) -> int:
    api_base, api_key, tenant_id = _resolve_config(args)
    status, payload = _json_request("GET", api_base + "/infra/audit/templates", api_key, tenant_id=tenant_id)
    data = _unwrap_data(status, payload, "audit_templates_failed")
    print(json.dumps(data, indent=2))
    return 0


def _cmd_run(args: argparse.Namespace) -> int:
    api_base, api_key, tenant_id = _resolve_config(args)
    data = _create_audit_run(
        api_base,
        tenant_id,
        api_key,
        template_id=args.template_id.strip().upper(),
        target_adapter=args.target_adapter.strip(),
        source_run_id=args.source_run_id.strip(),
        mitigation_profile=_load_json_file_or_arg(args.mitigation_profile),
        failure_profile=_load_json_file_or_arg(args.failure_profile),
        metadata=_load_json_file_or_arg(args.metadata),
    )
    print(json.dumps(data, indent=2))
    return 0


def _cmd_replay(args: argparse.Namespace) -> int:
    api_base, api_key, tenant_id = _resolve_config(args)
    status, payload = _json_request(
        "POST",
        api_base + "/infra/audit/replay",
        api_key,
        tenant_id=tenant_id,
        body={"tenant_id": tenant_id, "audit_run_id": args.audit_run_id.strip()},
        timeout_s=60.0,
    )
    data = _unwrap_data(status, payload, "audit_replay_failed")
    print(json.dumps(data, indent=2))
    return 0


def _cmd_quickstart(args: argparse.Namespace) -> int:
    api_base, api_key, tenant_id = _resolve_config(args)
    recipe = args.recipe.strip().lower()
    source_run_id = _create_run(api_base, tenant_id, api_key, f"kybernis-audit://{recipe}/{uuid.uuid4()}")
    idempotency_key = f"intent:kybernis-audit:{recipe}:{uuid.uuid4().hex}"
    token = uuid.uuid4().hex
    intent = _new_intent(token, recipe)
    template_id = "DARE"
    failure_profile: dict[str, Any] = {"recipe": recipe}
    metadata: dict[str, Any] = {"started_by": "kybernis-audit", "recipe": recipe}
    guard = {}
    replay = {}
    compensation_id = ""
    gate_id = ""

    if recipe in {"mutation", "ghost", "saga"}:
        outcome = "committed"
        if recipe == "ghost":
            outcome = "unknown"
            template_id = "GHOST"
            failure_profile.update({"type": "timeout_after_send", "expect_outcome_unknown": True})
        elif recipe == "saga":
            outcome = "failed"
            template_id = "SAGA"
            failure_profile.update({"type": "partial_workflow_failure", "expect_compensation": True})
        guard = _guard(
            api_base,
            tenant_id,
            api_key,
            source_run_id,
            idempotency_key,
            intent,
            original_payload={"source": "kybernis-audit", "phase": "initial", "recipe": recipe},
        )
        _resolve_mutation(
            api_base,
            tenant_id,
            api_key,
            str(guard["mutation_id"]),
            str(guard["runtime_intent_hash"]),
            provider="kybernis-audit",
            token=token,
            outcome=outcome,
        )
        if recipe == "saga":
            compensation_id = _create_compensation(api_base, tenant_id, api_key, str(guard["mutation_id"]))
            metadata["compensation_id"] = compensation_id
        replay = _guard(
            api_base,
            tenant_id,
            api_key,
            source_run_id,
            idempotency_key,
            intent,
            original_payload={"source": "kybernis-audit", "phase": "replay", "recipe": recipe},
        )
    elif recipe == "approval":
        template_id = "AUTH"
        _set_approval_policy(api_base, tenant_id, api_key, str(intent["tool_name"]))
        pre = _guard(
            api_base,
            tenant_id,
            api_key,
            source_run_id,
            idempotency_key,
            intent,
            original_payload={"source": "kybernis-audit", "phase": "approval_required", "recipe": recipe},
        )
        if str(pre.get("decision") or "") != "REQUIRES_APPROVAL":
            raise RuntimeError(f"approval_expected_requires_approval:{pre}")
        gate_id = str(pre.get("gate_id") or "")
        if not gate_id:
            raise RuntimeError("approval_missing_gate_id")
        _approve_gate(api_base, tenant_id, api_key, gate_id)
        intent["approval_gate_id"] = gate_id
        guard = _guard(
            api_base,
            tenant_id,
            api_key,
            source_run_id,
            idempotency_key,
            intent,
            original_payload={"source": "kybernis-audit", "phase": "approved_execution", "recipe": recipe},
        )
        _resolve_mutation(
            api_base,
            tenant_id,
            api_key,
            str(guard["mutation_id"]),
            str(guard["runtime_intent_hash"]),
            provider="kybernis-audit",
            token=token,
            outcome="committed",
        )
        replay = _guard(
            api_base,
            tenant_id,
            api_key,
            source_run_id,
            idempotency_key,
            intent,
            original_payload={"source": "kybernis-audit", "phase": "approval_replay", "recipe": recipe},
        )
        failure_profile.update({"type": "approval_bound_authorization", "requires_approval": True})
        metadata["gate_id"] = gate_id
    elif recipe == "race":
        template_id = "RACE"
        results = []
        for _ in range(2):
            results.append(
                _guard(
                    api_base,
                    tenant_id,
                    api_key,
                    source_run_id,
                    idempotency_key,
                    intent,
                    original_payload={"source": "kybernis-audit", "phase": "parallel_claim", "recipe": recipe},
                )
            )
        proceed = next((item for item in results if str(item.get("decision") or "") == "PROCEED"), None)
        suppressed = next((item for item in results if str(item.get("decision") or "") == "REPLAY_SUPPRESSED"), None)
        if proceed is None or suppressed is None:
            raise RuntimeError(f"race_recipe_failed:{results}")
        guard = proceed
        replay = suppressed
        _resolve_mutation(
            api_base,
            tenant_id,
            api_key,
            str(guard["mutation_id"]),
            str(guard["runtime_intent_hash"]),
            provider="kybernis-audit",
            token=token,
            outcome="committed",
        )
        failure_profile.update({"type": "parallel_duplicate_race", "expect_replay_suppressed": True})
        metadata["race_decisions"] = [str(item.get("decision") or "") for item in results]
    else:
        raise RuntimeError(f"unsupported_recipe:{recipe}")

    metadata.update(
        {
            "source_run_id": source_run_id,
            "mutation_id": str(guard.get("mutation_id") or ""),
            "idempotency_key": idempotency_key,
        }
    )
    run_data = _create_audit_run(
        api_base,
        tenant_id,
        api_key,
        template_id=template_id,
        target_adapter="manual",
        source_run_id=source_run_id,
        mitigation_profile={},
        failure_profile=failure_profile,
        metadata=metadata,
    )
    audit_run_id = str(run_data.get("audit_run_id") or "")
    report = _fetch_report(api_base, tenant_id, api_key, audit_run_id) if audit_run_id else {}
    print(
        json.dumps(
            {
                "recipe": recipe,
                "source_run_id": source_run_id,
                "guard_decision": str(guard.get("decision") or ""),
                "replay_decision": str(replay.get("decision") or ""),
                "mutation_id": str(guard.get("mutation_id") or ""),
                "gate_id": gate_id,
                "compensation_id": compensation_id,
                "audit_run": run_data,
                "report": report,
            },
            indent=2,
        )
    )
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Kybernis audit CLI")
    parser.add_argument("--api-base", default="")
    parser.add_argument("--api-key", default="")
    parser.add_argument("--tenant-id", default="")
    parser.add_argument("--env-file", action="append", default=[])
    sub = parser.add_subparsers(dest="command", required=True)

    sub.add_parser("templates", help="List available audit templates")

    run = sub.add_parser("run", help="Create an audit run")
    run.add_argument("--template-id", default="DARE")
    run.add_argument("--target-adapter", default="manual")
    run.add_argument("--source-run-id", required=True)
    run.add_argument("--mitigation-profile", default="{}")
    run.add_argument("--failure-profile", default="{}")
    run.add_argument("--metadata", default="{}")

    replay = sub.add_parser("replay", help="Replay an existing audit run")
    replay.add_argument("--audit-run-id", required=True)

    quickstart = sub.add_parser("quickstart", help="Run a productized manual audit recipe")
    quickstart.add_argument("--recipe", choices=["mutation", "ghost", "approval", "saga", "race"], default="mutation")

    return parser


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    for env_file in args.env_file:
        _import_dotenv(Path(env_file))

    try:
        if args.command == "templates":
            return _cmd_templates(args)
        if args.command == "run":
            return _cmd_run(args)
        if args.command == "replay":
            return _cmd_replay(args)
        if args.command == "quickstart":
            return _cmd_quickstart(args)
        raise RuntimeError(f"unsupported_command:{args.command}")
    except Exception as exc:
        print(str(exc), file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
