from __future__ import annotations

import json
import logging as py_logging
import socket
import sys
import threading
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


@dataclass(slots=True)
class LogEntry:
    timestamp: str
    stage_name: str
    status: str
    message: str
    error: str | None = None
    agent_tool: str | None = None
    attempt: int | None = None
    exit_status: int | None = None
    log_path: str | None = None
    cleanup_result: str | None = None


class CybervisorLogger:
    """Structured logger for human-readable stderr and JSON logs."""

    def __init__(self) -> None:
        self._stderr_logger = py_logging.getLogger("cybervisor.stderr")
        self._stderr_logger.handlers.clear()
        handler = py_logging.StreamHandler(sys.stderr)
        handler.setFormatter(py_logging.Formatter("%(levelname)s | %(message)s"))
        self._stderr_logger.addHandler(handler)
        self._stderr_logger.setLevel(py_logging.INFO)
        self._stderr_logger.propagate = False

    def log_to_stderr(self, message: str, level: str = "info") -> None:
        level_map: dict[str, int] = {
            "debug": py_logging.DEBUG,
            "info": py_logging.INFO,
            "warning": py_logging.WARNING,
            "error": py_logging.ERROR,
            "critical": py_logging.CRITICAL,
        }
        self._stderr_logger.log(level_map.get(level.lower(), py_logging.INFO), message)

    def make_entry(
        self,
        stage_name: str,
        status: str,
        message: str,
        error: str | None = None,
        agent_tool: str | None = None,
        attempt: int | None = None,
        exit_status: int | None = None,
        log_path: str | None = None,
        cleanup_result: str | None = None,
    ) -> LogEntry:
        return LogEntry(
            timestamp=datetime.now(tz=timezone.utc).isoformat(),
            stage_name=stage_name,
            status=status,
            message=message,
            error=error,
            agent_tool=agent_tool,
            attempt=attempt,
            exit_status=exit_status,
            log_path=log_path,
            cleanup_result=cleanup_result,
        )

    def log_to_json(self, entry: LogEntry | dict[str, Any], log_path: str | Path) -> None:
        path = Path(log_path)
        path.parent.mkdir(parents=True, exist_ok=True)
        payload = asdict(entry) if isinstance(entry, LogEntry) else entry
        with path.open("a", encoding="utf-8") as fh:
            fh.write(json.dumps(payload) + "\n")


def render_hook_event_summary(payload: dict[str, Any]) -> tuple[str, str]:
    event = str(payload.get("event", "hook_event"))
    agent_tool = payload.get("agent_tool")
    prefix = "[hook]"
    if isinstance(agent_tool, str) and agent_tool.strip():
        prefix = f"[hook][{agent_tool.strip()}]"
    decision = payload.get("decision")
    if not isinstance(decision, dict):
        decision = None
    error = payload.get("error")

    if event == "hook_invocation_started":
        return f"{prefix} invocation started", "info"
    if event == "hook_input_captured":
        return f"{prefix} input captured", "info"
    if event == "verifier_request_started":
        return f"{prefix} verifier request started", "info"
    if event == "verifier_response_received":
        return f"{prefix} verifier response received", "info"
    if event == "verifier_regeneration_requested":
        return f"{prefix} verifier regeneration requested", "warning"
    if event == "verifier_request_retry":
        if isinstance(error, str) and error:
            return f"{prefix} verifier request retry ({error})", "warning"
        return f"{prefix} verifier request retry", "warning"
    if event == "contract_validation_failed":
        if isinstance(error, str) and error:
            return f"{prefix} contract validation failed ({error})", "warning"
        return f"{prefix} contract validation failed", "warning"
    if event == "contract_validation_passed":
        return f"{prefix} contract validation passed", "info"
    if event == "final_attempt_bypass":
        return f"{prefix} final attempt bypass allowed stop", "warning"
    if event == "decision_emitted" and isinstance(decision, dict):
        verdict = decision.get("decision")
        reason: str | None = None
        candidate = decision.get("reason")
        if isinstance(candidate, str) and candidate.strip():
            reason = candidate.strip()
        if isinstance(verdict, str) and verdict:
            if reason:
                return f"{prefix} decision {verdict} ({reason})", "info"
            return f"{prefix} decision {verdict}", "info"
        return f"{prefix} decision emitted", "info"
    if event == "fallback_error_emitted":
        if isinstance(error, str) and error:
            return f"{prefix} fallback error ({error})", "error"
        return f"{prefix} fallback error", "error"
    message = payload.get("message")
    if isinstance(message, str) and message.strip():
        return f"{prefix} {message.strip()}", "info"
    return f"{prefix} {event}", "info"


class HookEventListener:
    def __init__(self, logger: CybervisorLogger, socket_path: str | Path = ".cybervisor/hook-events.sock") -> None:
        self.logger = logger
        self.socket_path = Path(socket_path)
        self._server: socket.socket | None = None
        self._thread: threading.Thread | None = None
        self._stop_event = threading.Event()
        self._decision_lock = threading.Lock()
        self._pending_blocks: list[dict[str, Any]] = []

    def _extract_hook_session_id(self, payload: dict[str, Any]) -> str | None:
        hook_input = payload.get("hook_input")
        if not isinstance(hook_input, str) or not hook_input.strip():
            return None
        try:
            decoded = json.loads(hook_input)
        except json.JSONDecodeError:
            return None
        if not isinstance(decoded, dict):
            return None
        session_id = decoded.get("session_id")
        return session_id if isinstance(session_id, str) and session_id.strip() else None

    def start(self) -> Path:
        self._stop_event.clear()
        self.socket_path.parent.mkdir(parents=True, exist_ok=True)
        self.socket_path.unlink(missing_ok=True)
        server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        server.bind(str(self.socket_path))
        server.listen()
        server.settimeout(0.2)
        self._server = server
        self._thread = threading.Thread(target=self._serve, name="hook-event-listener", daemon=True)
        self._thread.start()
        return self.socket_path

    def _serve(self) -> None:
        server = self._server
        if server is None:
            return
        while not self._stop_event.is_set():
            try:
                connection, _ = server.accept()
            except TimeoutError:
                continue
            except OSError:
                break
            with connection:
                try:
                    connection.settimeout(0.2)
                except (AttributeError, OSError):
                    pass
                data = bytearray()
                while not self._stop_event.is_set():
                    try:
                        chunk = connection.recv(4096)
                    except TimeoutError:
                        continue
                    except OSError:
                        break
                    if not chunk:
                        break
                    data.extend(chunk)
                for raw_line in data.decode("utf-8", errors="replace").splitlines():
                    line = raw_line.strip()
                    if not line:
                        continue
                    try:
                        payload = json.loads(line)
                    except json.JSONDecodeError:
                        self.logger.log_to_stderr("[hook] received malformed socket event", "warning")
                        continue
                    if not isinstance(payload, dict):
                        self.logger.log_to_stderr("[hook] received malformed socket event", "warning")
                        continue
                    message, level = render_hook_event_summary(payload)
                    self._record_blocking_decision(payload)
                    self.logger.log_to_stderr(message, level)

    def _record_blocking_decision(self, payload: dict[str, Any]) -> None:
        decision = payload.get("decision")
        if not isinstance(decision, dict):
            return
        agent_tool = payload.get("agent_tool")
        session_id = self._extract_hook_session_id(payload)
        verdict = decision.get("decision")
        reason = None
        if isinstance(decision.get("reason"), str) and str(decision.get("reason")).strip():
            reason = str(decision.get("reason"))

        is_blocking = verdict == "block"
        with self._decision_lock:
            if not is_blocking:
                self._pending_blocks = [
                    block
                    for block in self._pending_blocks
                    if not (
                        block.get("agent_tool") == agent_tool
                        and (
                            session_id is None
                            or block.get("session_id") == session_id
                        )
                    )
                ]
                return

            self._pending_blocks.append(
                {
                    "agent_tool": agent_tool,
                    "session_id": session_id,
                    "event": payload.get("event"),
                    "message": payload.get("message"),
                    "reason": reason,
                }
            )
        

    def pop_blocking_decision(self, agent_tool: str | None = None) -> dict[str, Any] | None:
        with self._decision_lock:
            for index, payload in enumerate(self._pending_blocks):
                payload_agent_tool = payload.get("agent_tool")
                if agent_tool is not None and payload_agent_tool != agent_tool:
                    continue
                return self._pending_blocks.pop(index)
        return None

    def clear_pending_blocks(self, agent_tool: str | None = None) -> None:
        with self._decision_lock:
            self._pending_blocks = [
                block for block in self._pending_blocks
                if agent_tool is not None and block.get("agent_tool") != agent_tool
            ]

    def stop(self) -> None:
        self._stop_event.set()
        if self._server is not None:
            try:
                with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
                    client.settimeout(0.1)
                    client.connect(str(self.socket_path))
            except OSError:
                pass
            self._server.close()
            self._server = None
        if self._thread is not None:
            self._thread.join(timeout=1.0)
            self._thread = None
        self.socket_path.unlink(missing_ok=True)
