"""
Failure Intelligence Engine
============================
Tracks, categorizes, and learns from service failures.
Recommends best recovery strategy based on historical success rates.

DB tables:
  service_failure_log   — every failure + recovery attempt
  service_health_snapshots — periodic health snapshots
"""

from __future__ import annotations

import json
import logging
import re
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Optional

logger = logging.getLogger("watchdog.intelligence")

_PROJECT_ROOT = Path(__file__).resolve().parent.parent
_DB_PATH = _PROJECT_ROOT / "watchdog" / "watchdog.db"

# ---------------------------------------------------------------------------
# Error categorization patterns
# ---------------------------------------------------------------------------

ERROR_PATTERNS = [
    # Bot-specific
    (re.compile(r"409.*Conflict|terminated by other getUpdates", re.I), "409_conflict"),
    (re.compile(r"NameError|name '.*' is not defined", re.I), "name_error"),
    (re.compile(r"ImportError|ModuleNotFoundError|No module named", re.I), "import_error"),
    (re.compile(r"AttributeError", re.I), "attribute_error"),
    (re.compile(r"SyntaxError", re.I), "syntax_error"),
    # Network / connectivity
    (re.compile(r"Connection refused|ConnectionRefusedError|WinError 10061|target machine actively refused", re.I), "connection_refused"),
    (re.compile(r"Connection reset|ConnectionResetError|BrokenPipeError", re.I), "connection_reset"),
    (re.compile(r"Timeout|timed?\s*out|ReadTimeout|ConnectTimeout", re.I), "timeout"),
    (re.compile(r"NetworkError|network", re.I), "network_error"),
    # Resource
    (re.compile(r"MemoryError|Out of memory|OOM|exit code 137", re.I), "out_of_memory"),
    (re.compile(r"disk\s*full|No space left|ENOSPC", re.I), "disk_full"),
    (re.compile(r"Too many open files|EMFILE", re.I), "file_descriptor_limit"),
    # Process
    (re.compile(r"process not found|no running", re.I), "process_missing"),
    (re.compile(r"permission denied|Access.*denied", re.I), "permission_denied"),
    (re.compile(r"port.*in use|Address already in use|EADDRINUSE", re.I), "port_in_use"),
    # Database
    (re.compile(r"database.*locked|OperationalError.*locked", re.I), "db_locked"),
    (re.compile(r"could not connect to server|psycopg|pg_", re.I), "db_connection"),
    # Docker
    (re.compile(r"docker.*not running|Cannot connect to the Docker", re.I), "docker_down"),
    (re.compile(r"container.*not found|No such container", re.I), "container_missing"),
]

# Default recovery strategies per error category
DEFAULT_STRATEGIES = {
    "409_conflict":     {"action": "kill_all_restart", "wait_seconds": 5, "description": "Kill all instances, wait for Telegram session release, restart"},
    "connection_refused": {"action": "restart_service", "wait_seconds": 3, "description": "Service not listening, restart it"},
    "connection_reset": {"action": "restart_service", "wait_seconds": 5, "description": "Connection dropped, restart service"},
    "timeout":          {"action": "restart_service", "wait_seconds": 10, "description": "Service unresponsive, restart after timeout"},
    "process_missing":  {"action": "restart_service", "wait_seconds": 2, "description": "Process died, restart immediately"},
    "port_in_use":      {"action": "kill_port_restart", "wait_seconds": 5, "description": "Kill process on port, then restart"},
    "out_of_memory":    {"action": "restart_service", "wait_seconds": 10, "description": "OOM killed, restart with delay"},
    "docker_down":      {"action": "start_docker", "wait_seconds": 30, "description": "Start Docker Desktop, then restart service"},
    "container_missing":{"action": "recreate_container", "wait_seconds": 10, "description": "Recreate Docker container"},
    "db_locked":        {"action": "wait_retry", "wait_seconds": 15, "description": "Database locked, wait and retry"},
    "db_connection":    {"action": "restart_service", "wait_seconds": 5, "description": "DB connection failed, restart DB"},
    "name_error":       {"action": "escalate", "wait_seconds": 0, "description": "Code error, cannot auto-fix — escalate to admin"},
    "import_error":     {"action": "escalate", "wait_seconds": 0, "description": "Missing module, cannot auto-fix — escalate to admin"},
    "attribute_error":  {"action": "escalate", "wait_seconds": 0, "description": "Code error — escalate to admin"},
    "syntax_error":     {"action": "escalate", "wait_seconds": 0, "description": "Code error — escalate to admin"},
    "disk_full":        {"action": "escalate", "wait_seconds": 0, "description": "Disk full — escalate to admin"},
    "permission_denied":{"action": "escalate", "wait_seconds": 0, "description": "Permission denied — escalate to admin"},
}


# ---------------------------------------------------------------------------
# Database setup
# ---------------------------------------------------------------------------

def _get_conn() -> sqlite3.Connection:
    _DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(str(_DB_PATH))
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    return conn


def init_db():
    """Create watchdog tables if they don't exist."""
    conn = _get_conn()
    conn.executescript("""
        CREATE TABLE IF NOT EXISTS service_failure_log (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            service TEXT NOT NULL,
            error_type TEXT NOT NULL,
            error_detail TEXT,
            recovery_action TEXT,
            recovery_success INTEGER DEFAULT 0,
            recovery_duration_ms INTEGER DEFAULT 0,
            consecutive_failures INTEGER DEFAULT 1,
            created_at TEXT DEFAULT (datetime('now'))
        );

        CREATE TABLE IF NOT EXISTS service_health_snapshots (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            snapshot_json TEXT NOT NULL,
            all_healthy INTEGER DEFAULT 0,
            created_at TEXT DEFAULT (datetime('now'))
        );

        CREATE INDEX IF NOT EXISTS idx_failure_service
            ON service_failure_log(service, created_at);
        CREATE INDEX IF NOT EXISTS idx_failure_type
            ON service_failure_log(error_type, recovery_success);
        CREATE INDEX IF NOT EXISTS idx_snapshot_time
            ON service_health_snapshots(created_at);
    """)
    conn.commit()
    conn.close()


# ---------------------------------------------------------------------------
# Error categorization
# ---------------------------------------------------------------------------

def categorize_error(service: str, error_string: str) -> str:
    """Classify raw error into a category."""
    if not error_string:
        return "unknown"
    for pattern, category in ERROR_PATTERNS:
        if pattern.search(error_string):
            return category
    return "unknown"


# ---------------------------------------------------------------------------
# Recovery strategy selection
# ---------------------------------------------------------------------------

def get_best_recovery(service: str, error_type: str) -> dict:
    """
    Get the best recovery strategy for this service + error combination.
    First checks historical success rates, then falls back to defaults.
    """
    conn = _get_conn()

    # Query: which recovery_action has highest success rate for this combo?
    rows = conn.execute("""
        SELECT recovery_action,
               COUNT(*) as total,
               SUM(recovery_success) as successes
        FROM service_failure_log
        WHERE service = ? AND error_type = ?
          AND created_at > datetime('now', '-7 days')
        GROUP BY recovery_action
        HAVING total >= 2
        ORDER BY (CAST(successes AS REAL) / total) DESC, total DESC
        LIMIT 1
    """, (service, error_type)).fetchall()
    conn.close()

    if rows:
        best = rows[0]
        success_rate = best["successes"] / best["total"]
        if success_rate > 0.3:  # Only use if > 30% success
            return {
                "action": best["recovery_action"],
                "source": "learned",
                "success_rate": round(success_rate, 2),
                "total_attempts": best["total"],
                "description": f"Learned strategy ({success_rate:.0%} success over {best['total']} attempts)",
            }

    # Fall back to default
    default = DEFAULT_STRATEGIES.get(error_type, DEFAULT_STRATEGIES.get("process_missing"))
    return {**default, "source": "default"}


# ---------------------------------------------------------------------------
# Recording
# ---------------------------------------------------------------------------

def record_failure(service: str, error_type: str, error_detail: str,
                   recovery_action: str, recovery_success: bool,
                   recovery_duration_ms: int = 0, consecutive: int = 1):
    """Record a failure and recovery attempt."""
    conn = _get_conn()
    conn.execute("""
        INSERT INTO service_failure_log
            (service, error_type, error_detail, recovery_action,
             recovery_success, recovery_duration_ms, consecutive_failures)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    """, (service, error_type, error_detail[:500] if error_detail else "",
          recovery_action, 1 if recovery_success else 0,
          recovery_duration_ms, consecutive))
    conn.commit()
    conn.close()


def record_snapshot(snapshot: dict, all_healthy: bool):
    """Record a health snapshot."""
    conn = _get_conn()
    conn.execute("""
        INSERT INTO service_health_snapshots (snapshot_json, all_healthy)
        VALUES (?, ?)
    """, (json.dumps(snapshot), 1 if all_healthy else 0))
    # Keep only last 7 days of snapshots
    conn.execute("""
        DELETE FROM service_health_snapshots
        WHERE created_at < datetime('now', '-7 days')
    """)
    conn.commit()
    conn.close()


# ---------------------------------------------------------------------------
# Analysis
# ---------------------------------------------------------------------------

def get_failure_summary(hours: int = 24) -> dict:
    """Get categorized failure counts for the last N hours."""
    conn = _get_conn()
    cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).strftime("%Y-%m-%d %H:%M:%S")

    # By service
    by_service = conn.execute("""
        SELECT service, COUNT(*) as total,
               SUM(recovery_success) as recovered,
               MAX(consecutive_failures) as max_consecutive
        FROM service_failure_log
        WHERE created_at > ?
        GROUP BY service
        ORDER BY total DESC
    """, (cutoff,)).fetchall()

    # By error type
    by_type = conn.execute("""
        SELECT error_type, COUNT(*) as total,
               SUM(recovery_success) as recovered
        FROM service_failure_log
        WHERE created_at > ?
        GROUP BY error_type
        ORDER BY total DESC
    """, (cutoff,)).fetchall()

    # Top recurring (same service + error_type combos)
    recurring = conn.execute("""
        SELECT service, error_type, COUNT(*) as count,
               ROUND(AVG(recovery_success) * 100) as recovery_pct
        FROM service_failure_log
        WHERE created_at > ?
        GROUP BY service, error_type
        HAVING count >= 3
        ORDER BY count DESC
        LIMIT 10
    """, (cutoff,)).fetchall()

    # Uptime percentage
    snapshots = conn.execute("""
        SELECT COUNT(*) as total,
               SUM(all_healthy) as healthy
        FROM service_health_snapshots
        WHERE created_at > ?
    """, (cutoff,)).fetchone()

    conn.close()

    uptime_pct = 0
    if snapshots and snapshots["total"] > 0:
        uptime_pct = round(snapshots["healthy"] / snapshots["total"] * 100, 1)

    return {
        "period_hours": hours,
        "uptime_percent": uptime_pct,
        "by_service": [dict(r) for r in by_service],
        "by_error_type": [dict(r) for r in by_type],
        "recurring_issues": [dict(r) for r in recurring],
    }


def should_escalate(service: str, consecutive: int, max_auto: int = 5) -> bool:
    """Should we alert the admin instead of auto-recovering?"""
    if consecutive >= max_auto:
        return True

    # Check if best strategy has been failing
    conn = _get_conn()
    recent = conn.execute("""
        SELECT COUNT(*) as total, SUM(recovery_success) as ok
        FROM service_failure_log
        WHERE service = ? AND created_at > datetime('now', '-1 hour')
    """, (service,)).fetchone()
    conn.close()

    if recent and recent["total"] >= 3 and recent["ok"] == 0:
        return True  # Everything failed in the last hour

    return False


def get_health_report() -> dict:
    """Generate a comprehensive health report for upstream reporting."""
    summary = get_failure_summary(24)

    conn = _get_conn()
    # Latest snapshot
    latest = conn.execute("""
        SELECT snapshot_json, all_healthy, created_at
        FROM service_health_snapshots
        ORDER BY id DESC LIMIT 1
    """).fetchone()

    # Total failures ever
    totals = conn.execute("""
        SELECT COUNT(*) as total_failures,
               SUM(recovery_success) as total_recovered
        FROM service_failure_log
    """).fetchone()
    conn.close()

    return {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "current_status": json.loads(latest["snapshot_json"]) if latest else {},
        "all_healthy": bool(latest["all_healthy"]) if latest else False,
        "last_check": latest["created_at"] if latest else None,
        "summary_24h": summary,
        "lifetime": {
            "total_failures": totals["total_failures"] if totals else 0,
            "total_recovered": totals["total_recovered"] if totals else 0,
        },
    }


# Initialize on import
init_db()
