"""
Hermes Bridge — Adapter layer providing Hermes-compatible interfaces for report_agent.

Ports essential patterns from the Hermes Agent core without importing hermes-agent-repo
directly (avoiding its complex dependency tree). Each component is a self-contained
adaptation of the corresponding Hermes module:

  - FailoverReason / ClassifiedError  ← agent/error_classifier.py
  - ReportSessionDB                   ← hermes_state.py (SessionDB)
  - Cost estimation helpers           ← agent/insights.py / usage_pricing.py

Usage:
    from agent.report_agent.hermes_bridge import (
        FailoverReason, ClassifiedError, classify_error,
        ReportSessionDB,
    )
"""

from __future__ import annotations

import enum
import json
import logging
import os
import sqlite3
import threading
import time
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, TypeVar

logger = logging.getLogger(__name__)

T = TypeVar("T")

# ─── Paths ──────────────────────────────────────────────────────────────

def _get_report_db_path() -> Path:
    """Get the path to the report agent's SQLite state database."""
    base = Path(os.path.dirname(__file__)).parent.parent  # backend/
    db_dir = base / "data"
    db_dir.mkdir(parents=True, exist_ok=True)
    return db_dir / "report_state.db"


# ─── Error Taxonomy (from error_classifier.py) ──────────────────────────

class FailoverReason(enum.Enum):
    """Why an API call failed — determines recovery strategy.

    Ported from hermes-agent-repo/agent/error_classifier.py.
    """
    # Authentication
    auth = "auth"
    auth_permanent = "auth_permanent"

    # Billing / quota
    billing = "billing"
    rate_limit = "rate_limit"

    # Server-side
    overloaded = "overloaded"
    server_error = "server_error"

    # Transport
    timeout = "timeout"

    # Context / payload
    context_overflow = "context_overflow"
    payload_too_large = "payload_too_large"

    # Model
    model_not_found = "model_not_found"

    # Request format
    format_error = "format_error"

    # Catch-all
    unknown = "unknown"


@dataclass
class ClassifiedError:
    """Structured classification of an API error with recovery hints.

    Ported from hermes-agent-repo/agent/error_classifier.py.
    """
    reason: FailoverReason
    status_code: Optional[int] = None
    provider: Optional[str] = None
    model: Optional[str] = None
    message: str = ""
    error_context: Dict[str, Any] = field(default_factory=dict)

    # Recovery action hints
    retryable: bool = True
    should_compress: bool = False
    should_rotate: bool = False
    max_retries: int = 3
    backoff_seconds: float = 1.0


def classify_error(exc: Exception, *, provider: str = "codex") -> ClassifiedError:
    """Classify an API error into a FailoverReason with recovery hints.

    Simplified port of error_classifier.py's pipeline, focused on
    Codex and OpenAI error patterns relevant to report_agent.
    """
    import httpx

    msg = str(exc).lower()
    status = None

    if isinstance(exc, httpx.HTTPStatusError):
        status = exc.response.status_code if exc.response else None
        body = ""
        try:
            body = exc.response.text[:500].lower()
        except Exception:
            pass

        if status == 401 or status == 403:
            return ClassifiedError(
                reason=FailoverReason.auth,
                status_code=status, provider=provider, message=str(exc),
                retryable=False, should_rotate=True,
            )
        if status == 402:
            return ClassifiedError(
                reason=FailoverReason.billing,
                status_code=status, provider=provider, message=str(exc),
                retryable=False, should_rotate=True,
            )
        if status == 429:
            return ClassifiedError(
                reason=FailoverReason.rate_limit,
                status_code=status, provider=provider, message=str(exc),
                retryable=True, backoff_seconds=5.0, max_retries=5,
            )
        if status == 413 or "too large" in body:
            return ClassifiedError(
                reason=FailoverReason.payload_too_large,
                status_code=status, provider=provider, message=str(exc),
                retryable=True, should_compress=True,
            )
        if status == 404 or "model" in body and "not found" in body:
            return ClassifiedError(
                reason=FailoverReason.model_not_found,
                status_code=status, provider=provider, message=str(exc),
                retryable=False,
            )
        if status == 503 or status == 529:
            return ClassifiedError(
                reason=FailoverReason.overloaded,
                status_code=status, provider=provider, message=str(exc),
                retryable=True, backoff_seconds=10.0,
            )
        if status and 500 <= status < 600:
            return ClassifiedError(
                reason=FailoverReason.server_error,
                status_code=status, provider=provider, message=str(exc),
                retryable=True, backoff_seconds=3.0,
            )

    # Transport errors
    if isinstance(exc, httpx.RequestError):
        if "timeout" in msg or "timed out" in msg:
            return ClassifiedError(
                reason=FailoverReason.timeout,
                provider=provider, message=str(exc),
                retryable=True, backoff_seconds=2.0,
            )
        return ClassifiedError(
            reason=FailoverReason.server_error,
            provider=provider, message=str(exc),
            retryable=True, backoff_seconds=2.0,
        )

    # Context overflow patterns
    if any(kw in msg for kw in ("context length", "token limit", "maximum context", "too many tokens")):
        return ClassifiedError(
            reason=FailoverReason.context_overflow,
            provider=provider, message=str(exc),
            retryable=True, should_compress=True,
        )

    # Rate limit patterns in message
    if "rate limit" in msg or "quota" in msg or "too many requests" in msg:
        return ClassifiedError(
            reason=FailoverReason.rate_limit,
            provider=provider, message=str(exc),
            retryable=True, backoff_seconds=5.0, max_retries=5,
        )

    return ClassifiedError(
        reason=FailoverReason.unknown,
        provider=provider, message=str(exc),
        retryable=True, backoff_seconds=2.0,
    )


# ─── Report Session DB (from hermes_state.py) ───────────────────────────

REPORT_SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS report_sessions (
    id TEXT PRIMARY KEY,
    question TEXT NOT NULL,
    model TEXT,
    status TEXT DEFAULT 'pending',
    started_at REAL NOT NULL,
    ended_at REAL,
    cycles_count INTEGER DEFAULT 0,
    tools_used TEXT,
    input_tokens INTEGER DEFAULT 0,
    output_tokens INTEGER DEFAULT 0,
    error_count INTEGER DEFAULT 0,
    compression_count INTEGER DEFAULT 0,
    report_id INTEGER,
    generation_time_ms INTEGER,
    html_length INTEGER DEFAULT 0,
    metadata TEXT
);

CREATE TABLE IF NOT EXISTS report_events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES report_sessions(id),
    event_type TEXT NOT NULL,
    event_data TEXT,
    timestamp REAL NOT NULL
);

CREATE TABLE IF NOT EXISTS report_metrics (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    metric_name TEXT NOT NULL,
    metric_value REAL,
    recorded_at REAL NOT NULL,
    metadata TEXT
);

CREATE INDEX IF NOT EXISTS idx_report_sessions_started
    ON report_sessions(started_at DESC);
CREATE INDEX IF NOT EXISTS idx_report_events_session
    ON report_events(session_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_report_metrics_name
    ON report_metrics(metric_name, recorded_at DESC);
"""


class ReportSessionDB:
    """SQLite-backed session storage for report agent lifecycle tracking.

    Adapted from hermes_state.py SessionDB with simplified schema for
    report-specific metrics.
    """

    def __init__(self, db_path: Path = None):
        self.db_path = db_path or _get_report_db_path()
        self.db_path.parent.mkdir(parents=True, exist_ok=True)

        self._lock = threading.Lock()
        self._conn = sqlite3.connect(
            str(self.db_path),
            check_same_thread=False,
            timeout=5.0,
            isolation_level=None,
        )
        self._conn.row_factory = sqlite3.Row

        # Try WAL mode (best for concurrent access)
        try:
            self._conn.execute("PRAGMA journal_mode=WAL")
        except sqlite3.OperationalError:
            self._conn.execute("PRAGMA journal_mode=DELETE")

        self._conn.execute("PRAGMA foreign_keys=ON")
        self._conn.executescript(REPORT_SCHEMA_SQL)
        self._conn.commit()

    def close(self):
        """Close the database connection."""
        with self._lock:
            if self._conn:
                try:
                    self._conn.execute("PRAGMA wal_checkpoint(PASSIVE)")
                except Exception:
                    pass
                self._conn.close()
                self._conn = None

    def _execute_write(self, fn: Callable[[sqlite3.Connection], T]) -> T:
        """Execute a write transaction with locking."""
        with self._lock:
            self._conn.execute("BEGIN IMMEDIATE")
            try:
                result = fn(self._conn)
                self._conn.commit()
                return result
            except BaseException:
                try:
                    self._conn.rollback()
                except Exception:
                    pass
                raise

    # ── Session lifecycle ──

    def create_session(
        self,
        session_id: str,
        question: str,
        model: str = None,
    ) -> str:
        """Create a new report session."""
        def _do(conn):
            conn.execute(
                """INSERT OR IGNORE INTO report_sessions
                   (id, question, model, status, started_at)
                   VALUES (?, ?, ?, 'running', ?)""",
                (session_id, question, model, time.time()),
            )
        self._execute_write(_do)
        return session_id

    def end_session(
        self,
        session_id: str,
        *,
        status: str = "done",
        cycles_count: int = 0,
        tools_used: List[str] = None,
        input_tokens: int = 0,
        output_tokens: int = 0,
        error_count: int = 0,
        compression_count: int = 0,
        report_id: int = None,
        generation_time_ms: int = 0,
        html_length: int = 0,
    ) -> None:
        """Mark a session as ended with final metrics."""
        def _do(conn):
            conn.execute(
                """UPDATE report_sessions SET
                   ended_at = ?, status = ?, cycles_count = ?,
                   tools_used = ?, input_tokens = ?, output_tokens = ?,
                   error_count = ?, compression_count = ?,
                   report_id = ?, generation_time_ms = ?, html_length = ?
                   WHERE id = ?""",
                (
                    time.time(), status, cycles_count,
                    json.dumps(tools_used or []), input_tokens, output_tokens,
                    error_count, compression_count,
                    report_id, generation_time_ms, html_length,
                    session_id,
                ),
            )
        self._execute_write(_do)

    def log_event(
        self,
        session_id: str,
        event_type: str,
        event_data: Dict[str, Any] = None,
    ) -> None:
        """Log a timestamped event for a session."""
        def _do(conn):
            conn.execute(
                """INSERT INTO report_events (session_id, event_type, event_data, timestamp)
                   VALUES (?, ?, ?, ?)""",
                (session_id, event_type, json.dumps(event_data or {}), time.time()),
            )
        self._execute_write(_do)

    def record_metric(
        self,
        metric_name: str,
        metric_value: float,
        metadata: Dict[str, Any] = None,
    ) -> None:
        """Record a timestamped metric."""
        def _do(conn):
            conn.execute(
                """INSERT INTO report_metrics (metric_name, metric_value, recorded_at, metadata)
                   VALUES (?, ?, ?, ?)""",
                (metric_name, metric_value, time.time(), json.dumps(metadata or {})),
            )
        self._execute_write(_do)

    # ── Query helpers ──

    def get_sessions(
        self, limit: int = 50, status: str = None
    ) -> List[Dict[str, Any]]:
        """Get recent report sessions."""
        if status:
            cursor = self._conn.execute(
                "SELECT * FROM report_sessions WHERE status = ? "
                "ORDER BY started_at DESC LIMIT ?",
                (status, limit),
            )
        else:
            cursor = self._conn.execute(
                "SELECT * FROM report_sessions ORDER BY started_at DESC LIMIT ?",
                (limit,),
            )
        return [dict(row) for row in cursor.fetchall()]

    def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
        """Get a single session by ID."""
        cursor = self._conn.execute(
            "SELECT * FROM report_sessions WHERE id = ?", (session_id,),
        )
        row = cursor.fetchone()
        return dict(row) if row else None

    def get_events(
        self, session_id: str, limit: int = 100
    ) -> List[Dict[str, Any]]:
        """Get events for a session."""
        cursor = self._conn.execute(
            "SELECT * FROM report_events WHERE session_id = ? "
            "ORDER BY timestamp ASC LIMIT ?",
            (session_id, limit),
        )
        return [dict(row) for row in cursor.fetchall()]

    def get_metrics(
        self, metric_name: str, days: int = 30, limit: int = 1000
    ) -> List[Dict[str, Any]]:
        """Get metrics by name within a time window."""
        cutoff = time.time() - (days * 86400)
        cursor = self._conn.execute(
            "SELECT * FROM report_metrics WHERE metric_name = ? "
            "AND recorded_at >= ? ORDER BY recorded_at DESC LIMIT ?",
            (metric_name, cutoff, limit),
        )
        return [dict(row) for row in cursor.fetchall()]

    def get_overview(self, days: int = 30) -> Dict[str, Any]:
        """Compute overview statistics for the last N days."""
        cutoff = time.time() - (days * 86400)
        cursor = self._conn.execute(
            """SELECT
                COUNT(*) as total_sessions,
                SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as success_count,
                SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count,
                AVG(generation_time_ms) as avg_generation_ms,
                SUM(input_tokens) as total_input_tokens,
                SUM(output_tokens) as total_output_tokens,
                AVG(cycles_count) as avg_cycles,
                AVG(html_length) as avg_html_length,
                SUM(error_count) as total_errors,
                SUM(compression_count) as total_compressions
            FROM report_sessions
            WHERE started_at >= ?""",
            (cutoff,),
        )
        row = cursor.fetchone()
        return dict(row) if row else {}


# ─── Duration formatting (from insights.py) ─────────────────────────────

def format_duration(seconds: float) -> str:
    """Format seconds into a human-readable duration string."""
    if seconds < 60:
        return f"{seconds:.0f}s"
    if seconds < 3600:
        m = int(seconds // 60)
        s = int(seconds % 60)
        return f"{m}m{s}s" if s else f"{m}m"
    h = int(seconds // 3600)
    m = int((seconds % 3600) // 60)
    return f"{h}h{m}m" if m else f"{h}h"
