"""Cross-process confirmation bridge: systemd heartbeat service ↔ GTK console UI."""

from __future__ import annotations

from dataclasses import asdict
import json
import os
from pathlib import Path
import tempfile
import time
from typing import Any, Literal

from .command_models import CommandEnvelope, ConfirmationCallback, ConfirmationResult, PolicyDecision
from .double_confirm_flow import requires_double_confirm, wrap_confirmation_callback

PENDING_FILENAME = "pending-command-confirmation.json"
BridgeState = Literal["waiting", "awaiting_ui"]


def default_state_dir() -> Path:
    return Path.home() / ".config" / "iuve-connect-agent"


class ConfirmationBridge:
    """File-based IPC between the heartbeat service process and GTK UI."""

    def __init__(
        self,
        *,
        state_dir: Path | None = None,
        poll_interval_sec: float = 0.25,
        timeout_sec: float = 300.0,
    ) -> None:
        self._state_dir = (state_dir or default_state_dir()).expanduser()
        self._state_dir.mkdir(parents=True, exist_ok=True)
        self._poll_interval = max(0.05, poll_interval_sec)
        self._timeout = max(0.0, timeout_sec)
        self._ui_handler: ConfirmationCallback | None = None

    def register_ui_handler(self, callback: ConfirmationCallback | None) -> None:
        self._ui_handler = callback

    def request(self, envelope: CommandEnvelope, decision: PolicyDecision) -> ConfirmationResult:
        if self._ui_handler is not None:
            handler = wrap_confirmation_callback(self._ui_handler)
            try:
                return handler(envelope, decision)
            except Exception:
                return "decline"

        command_id = envelope.command_id
        self.publish_pending(envelope, decision, state="waiting")
        result = self.wait_for_response(command_id)
        if result == "defer":
            self.mark_awaiting_ui()
            return "defer"
        self.clear_request(command_id)
        return result

    def publish_pending(
        self,
        envelope: CommandEnvelope,
        decision: PolicyDecision,
        *,
        state: BridgeState = "waiting",
    ) -> None:
        payload = {
            "request_id": envelope.command_id,
            "command_id": envelope.command_id,
            "command_type": envelope.command_type,
            "payload": dict(envelope.payload),
            "requested_at": envelope.requested_at,
            "expires_at": envelope.expires_at,
            "state": state,
            "created_at": _utc_now_iso(),
            "decision": asdict(decision),
            "capability_key": decision.capability_key,
            "confirm_type": decision.confirm_type,
            "stop_zone": decision.stop_zone,
        }
        self._write_json(self._pending_path(), payload)

    def mark_awaiting_ui(self) -> None:
        pending = self.read_pending()
        if pending is None:
            return
        pending["state"] = "awaiting_ui"
        pending["awaiting_ui_since"] = _utc_now_iso()
        self._write_json(self._pending_path(), pending)

    def read_pending(self) -> dict[str, Any] | None:
        data = self._read_json(self._pending_path())
        return dict(data) if isinstance(data, dict) else None

    def list_pending(self, *, state: BridgeState | None = None) -> list[dict[str, Any]]:
        pending = self.read_pending()
        if pending is None:
            return []
        if state is not None and str(pending.get("state") or "") != state:
            return []
        return [pending]

    def submit_response(
        self,
        command_id: str,
        result: ConfirmationResult,
        *,
        always_allow_command_type: str = "",
    ) -> None:
        payload = {
            "request_id": command_id,
            "result": result,
            "always_allow_command_type": str(always_allow_command_type or "").strip(),
            "responded_at": _utc_now_iso(),
        }
        self._write_json(self._response_path(command_id), payload)
        self._mark_pending_awaiting_ui_if_waiting(command_id)

    def read_response(self, command_id: str) -> dict[str, Any] | None:
        data = self._read_json(self._response_path(command_id))
        return dict(data) if isinstance(data, dict) else None

    def wait_for_response(self, command_id: str) -> ConfirmationResult | Literal["defer"]:
        deadline = time.monotonic() + self._timeout
        while time.monotonic() < deadline:
            result = self._read_confirmation_result(command_id)
            if result is not None:
                return result
            time.sleep(self._poll_interval)
        result = self._read_confirmation_result(command_id)
        if result is not None:
            return result
        return "defer"

    def clear_request(self, command_id: str) -> None:
        try:
            self._pending_path().unlink(missing_ok=True)
        except TypeError:
            if self._pending_path().exists():
                self._pending_path().unlink()
        try:
            self._response_path(command_id).unlink(missing_ok=True)
        except TypeError:
            path = self._response_path(command_id)
            if path.exists():
                path.unlink()

    def build_service_callback(self) -> ConfirmationCallback:
        return self.request

    @staticmethod
    def envelope_from_request(request: dict[str, Any]) -> CommandEnvelope:
        return pending_to_envelope(request)

    @staticmethod
    def decision_from_request(request: dict[str, Any]) -> PolicyDecision:
        return pending_to_decision(request)

    def _mark_pending_awaiting_ui_if_waiting(self, command_id: str) -> None:
        pending = self.read_pending()
        if pending is None:
            return
        pending_id = str(
            pending.get("command_id") or pending.get("request_id") or ""
        ).strip()
        if pending_id != command_id:
            return
        if str(pending.get("state") or "") != "waiting":
            return
        pending["state"] = "awaiting_ui"
        pending["awaiting_ui_since"] = _utc_now_iso()
        self._write_json(self._pending_path(), pending)

    def _read_confirmation_result(
        self,
        command_id: str,
    ) -> ConfirmationResult | None:
        response = self.read_response(command_id)
        if not isinstance(response, dict):
            return None
        result = str(response.get("result") or "").strip().lower()
        if result in {"approve", "decline"}:
            return result  # type: ignore[return-value]
        return None

    def _pending_path(self) -> Path:
        return self._state_dir / PENDING_FILENAME

    def _response_path(self, command_id: str) -> Path:
        return self._state_dir / f"confirmation-response-{command_id}.json"

    def _write_json(self, path: Path, payload: dict[str, Any]) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        encoded = json.dumps(payload, ensure_ascii=True, sort_keys=True, indent=2)
        fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
        temp_path = Path(temp_name)
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as handle:
                handle.write(encoded)
                handle.flush()
                os.fsync(handle.fileno())
            os.replace(temp_path, path)
        finally:
            if temp_path.exists():
                temp_path.unlink(missing_ok=True)

    @staticmethod
    def _read_json(path: Path) -> Any:
        if not path.exists():
            return None
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            return None


def build_service_confirmation_callback(
    *,
    state_dir: Path | None = None,
    timeout_sec: float = 300.0,
) -> ConfirmationCallback:
    return ConfirmationBridge(state_dir=state_dir, timeout_sec=timeout_sec).request


def pending_to_envelope(pending: dict[str, Any]) -> CommandEnvelope:
    return CommandEnvelope(
        command_id=str(pending.get("command_id") or pending.get("request_id") or "").strip(),
        command_type=str(pending.get("command_type") or "").strip(),
        payload=dict(pending.get("payload") or {}),
        requested_at=str(pending.get("requested_at") or ""),
        expires_at=str(pending.get("expires_at") or ""),
    )


def pending_to_decision(pending: dict[str, Any]) -> PolicyDecision:
    raw = pending.get("decision")
    if isinstance(raw, dict):
        return PolicyDecision(
            allowed=bool(raw.get("allowed", True)),
            reason_code=str(raw.get("reason_code") or ""),
            error_message=str(raw.get("error_message") or ""),
            require_confirmation=bool(raw.get("require_confirmation", True)),
            capability_key=str(raw.get("capability_key") or pending.get("capability_key") or ""),
            confirm_type=str(raw.get("confirm_type") or pending.get("confirm_type") or "none"),
            stop_zone=raw.get("stop_zone") or pending.get("stop_zone"),
        )
    return PolicyDecision(
        allowed=True,
        require_confirmation=True,
        capability_key=str(pending.get("capability_key") or ""),
    )


def _utc_now_iso() -> str:
    from datetime import datetime, timezone

    return datetime.now(tz=timezone.utc).isoformat()
