"""SQLite store for drift history, baselines, and policies."""

from __future__ import annotations

import json
import sqlite3
import time
from contextlib import contextmanager
from dataclasses import asdict, dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Generator, Optional


class DriftSeverity(str, Enum):
    """Severity level for a detected drift."""
    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"
    INFO = "info"


class DriftCategory(str, Enum):
    """Category of drift."""
    SECURITY = "security"
    COST = "cost"
    COMPLIANCE = "compliance"
    CONFIGURATION = "configuration"
    STRUCTURAL = "structural"


class DriftClassification(str, Enum):
    """Classification of drift origin."""
    UNAUTHORIZED = "unauthorized"
    INTENTIONAL = "intentional"
    EMERGENCY = "emergency"
    UNKNOWN = "unknown"


class RemediationStatus(str, Enum):
    """Status of remediation for a drift."""
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    REMEDIATED = "remediated"
    ACCEPTED = "accepted"
    IGNORED = "ignored"


@dataclass
class DriftRecord:
    """A single detected drift between desired and actual state."""
    id: str
    scan_id: str
    provider: str  # azure, aws, gcp
    resource_type: str
    resource_id: str
    resource_name: str
    attribute: str
    desired_value: Any
    actual_value: Any
    severity: DriftSeverity
    category: DriftCategory
    classification: DriftClassification = DriftClassification.UNKNOWN
    remediation_status: RemediationStatus = RemediationStatus.PENDING
    remediation_code: Optional[str] = None
    pr_url: Optional[str] = None
    detected_at: float = field(default_factory=time.time)
    resolved_at: Optional[float] = None
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        d = asdict(self)
        d["severity"] = self.severity.value
        d["category"] = self.category.value
        d["classification"] = self.classification.value
        d["remediation_status"] = self.remediation_status.value
        return d


@dataclass
class ScanRecord:
    """A drift scan execution record."""
    id: str
    provider: str
    iac_source: str  # terraform, pulumi, cloudformation
    state_path: str
    total_resources: int
    drifted_resources: int
    new_resources: int
    deleted_resources: int
    started_at: float
    completed_at: float
    drift_ids: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return asdict(self)


@dataclass
class BaselineEntry:
    """A baseline entry representing known-good or accepted state."""
    resource_id: str
    resource_type: str
    attribute: str
    accepted_value: Any
    reason: str
    created_at: float = field(default_factory=time.time)
    expires_at: Optional[float] = None


_SCHEMA = """
CREATE TABLE IF NOT EXISTS drift_records (
    id TEXT PRIMARY KEY,
    scan_id TEXT NOT NULL,
    provider TEXT NOT NULL,
    resource_type TEXT NOT NULL,
    resource_id TEXT NOT NULL,
    resource_name TEXT NOT NULL,
    attribute TEXT NOT NULL,
    desired_value TEXT,
    actual_value TEXT,
    severity TEXT NOT NULL,
    category TEXT NOT NULL,
    classification TEXT NOT NULL DEFAULT 'unknown',
    remediation_status TEXT NOT NULL DEFAULT 'pending',
    remediation_code TEXT,
    pr_url TEXT,
    detected_at REAL NOT NULL,
    resolved_at REAL,
    metadata TEXT DEFAULT '{}'
);

CREATE TABLE IF NOT EXISTS scan_records (
    id TEXT PRIMARY KEY,
    provider TEXT NOT NULL,
    iac_source TEXT NOT NULL,
    state_path TEXT NOT NULL,
    total_resources INTEGER NOT NULL,
    drifted_resources INTEGER NOT NULL,
    new_resources INTEGER NOT NULL,
    deleted_resources INTEGER NOT NULL,
    started_at REAL NOT NULL,
    completed_at REAL NOT NULL,
    drift_ids TEXT DEFAULT '[]'
);

CREATE TABLE IF NOT EXISTS baselines (
    resource_id TEXT NOT NULL,
    resource_type TEXT NOT NULL,
    attribute TEXT NOT NULL,
    accepted_value TEXT,
    reason TEXT NOT NULL,
    created_at REAL NOT NULL,
    expires_at REAL,
    PRIMARY KEY (resource_id, attribute)
);

CREATE TABLE IF NOT EXISTS policies (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    category TEXT NOT NULL,
    severity TEXT NOT NULL,
    enabled INTEGER NOT NULL DEFAULT 1,
    config TEXT DEFAULT '{}',
    created_at REAL NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_drift_scan ON drift_records(scan_id);
CREATE INDEX IF NOT EXISTS idx_drift_provider ON drift_records(provider);
CREATE INDEX IF NOT EXISTS idx_drift_severity ON drift_records(severity);
CREATE INDEX IF NOT EXISTS idx_drift_status ON drift_records(remediation_status);
CREATE INDEX IF NOT EXISTS idx_scan_provider ON scan_records(provider);
"""


class DriftStore:
    """SQLite-backed store for drift detection data."""

    def __init__(self, db_path: str | Path = "driftguard.db") -> None:
        self.db_path = Path(db_path)
        self._init_db()

    def _init_db(self) -> None:
        """Initialize database schema."""
        with self._conn() as conn:
            conn.executescript(_SCHEMA)

    @contextmanager
    def _conn(self) -> Generator[sqlite3.Connection, None, None]:
        """Context manager for database connections."""
        conn = sqlite3.connect(str(self.db_path))
        conn.row_factory = sqlite3.Row
        try:
            yield conn
            conn.commit()
        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()

    def save_drift(self, record: DriftRecord) -> None:
        """Save a drift record."""
        with self._conn() as conn:
            conn.execute(
                """INSERT OR REPLACE INTO drift_records
                   (id, scan_id, provider, resource_type, resource_id, resource_name,
                    attribute, desired_value, actual_value, severity, category,
                    classification, remediation_status, remediation_code, pr_url,
                    detected_at, resolved_at, metadata)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                (
                    record.id, record.scan_id, record.provider, record.resource_type,
                    record.resource_id, record.resource_name, record.attribute,
                    json.dumps(record.desired_value), json.dumps(record.actual_value),
                    record.severity.value, record.category.value,
                    record.classification.value, record.remediation_status.value,
                    record.remediation_code, record.pr_url,
                    record.detected_at, record.resolved_at,
                    json.dumps(record.metadata),
                ),
            )

    def save_scan(self, record: ScanRecord) -> None:
        """Save a scan record."""
        with self._conn() as conn:
            conn.execute(
                """INSERT OR REPLACE INTO scan_records
                   (id, provider, iac_source, state_path, total_resources,
                    drifted_resources, new_resources, deleted_resources,
                    started_at, completed_at, drift_ids)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                (
                    record.id, record.provider, record.iac_source, record.state_path,
                    record.total_resources, record.drifted_resources,
                    record.new_resources, record.deleted_resources,
                    record.started_at, record.completed_at,
                    json.dumps(record.drift_ids),
                ),
            )

    def get_drifts(
        self,
        scan_id: Optional[str] = None,
        provider: Optional[str] = None,
        severity: Optional[DriftSeverity] = None,
        status: Optional[RemediationStatus] = None,
        limit: int = 100,
    ) -> list[dict[str, Any]]:
        """Query drift records with optional filters."""
        query = "SELECT * FROM drift_records WHERE 1=1"
        params: list[Any] = []
        if scan_id:
            query += " AND scan_id = ?"
            params.append(scan_id)
        if provider:
            query += " AND provider = ?"
            params.append(provider)
        if severity:
            query += " AND severity = ?"
            params.append(severity.value)
        if status:
            query += " AND remediation_status = ?"
            params.append(status.value)
        query += " ORDER BY detected_at DESC LIMIT ?"
        params.append(limit)
        with self._conn() as conn:
            rows = conn.execute(query, params).fetchall()
        results = []
        for row in rows:
            d = dict(row)
            d["desired_value"] = json.loads(d["desired_value"]) if d["desired_value"] else None
            d["actual_value"] = json.loads(d["actual_value"]) if d["actual_value"] else None
            d["metadata"] = json.loads(d["metadata"]) if d["metadata"] else {}
            results.append(d)
        return results

    def get_scans(self, limit: int = 50) -> list[dict[str, Any]]:
        """Get recent scan records."""
        with self._conn() as conn:
            rows = conn.execute(
                "SELECT * FROM scan_records ORDER BY completed_at DESC LIMIT ?",
                (limit,),
            ).fetchall()
        results = []
        for row in rows:
            d = dict(row)
            d["drift_ids"] = json.loads(d["drift_ids"]) if d["drift_ids"] else []
            results.append(d)
        return results

    def update_classification(self, drift_id: str, classification: DriftClassification) -> None:
        """Update drift classification."""
        with self._conn() as conn:
            conn.execute(
                "UPDATE drift_records SET classification = ? WHERE id = ?",
                (classification.value, drift_id),
            )

    def update_remediation(
        self,
        drift_id: str,
        status: RemediationStatus,
        code: Optional[str] = None,
        pr_url: Optional[str] = None,
    ) -> None:
        """Update remediation status and optionally attach code/PR."""
        with self._conn() as conn:
            resolved_at = time.time() if status in (RemediationStatus.REMEDIATED, RemediationStatus.ACCEPTED) else None
            conn.execute(
                """UPDATE drift_records
                   SET remediation_status = ?, remediation_code = COALESCE(?, remediation_code),
                       pr_url = COALESCE(?, pr_url), resolved_at = COALESCE(?, resolved_at)
                   WHERE id = ?""",
                (status.value, code, pr_url, resolved_at, drift_id),
            )

    def save_baseline(self, entry: BaselineEntry) -> None:
        """Save a baseline entry."""
        with self._conn() as conn:
            conn.execute(
                """INSERT OR REPLACE INTO baselines
                   (resource_id, resource_type, attribute, accepted_value, reason, created_at, expires_at)
                   VALUES (?, ?, ?, ?, ?, ?, ?)""",
                (
                    entry.resource_id, entry.resource_type, entry.attribute,
                    json.dumps(entry.accepted_value), entry.reason,
                    entry.created_at, entry.expires_at,
                ),
            )

    def get_baseline(self, resource_id: str, attribute: str) -> Optional[dict[str, Any]]:
        """Get baseline for a resource attribute."""
        with self._conn() as conn:
            row = conn.execute(
                "SELECT * FROM baselines WHERE resource_id = ? AND attribute = ?",
                (resource_id, attribute),
            ).fetchone()
        if row:
            d = dict(row)
            d["accepted_value"] = json.loads(d["accepted_value"]) if d["accepted_value"] else None
            # Check expiry
            if d.get("expires_at") and d["expires_at"] < time.time():
                return None
            return d
        return None

    def get_all_baselines(self) -> list[dict[str, Any]]:
        """Get all active baselines."""
        now = time.time()
        with self._conn() as conn:
            rows = conn.execute(
                "SELECT * FROM baselines WHERE expires_at IS NULL OR expires_at > ?",
                (now,),
            ).fetchall()
        results = []
        for row in rows:
            d = dict(row)
            d["accepted_value"] = json.loads(d["accepted_value"]) if d["accepted_value"] else None
            results.append(d)
        return results

    def get_drift_trends(self, days: int = 30) -> dict[str, Any]:
        """Get drift trend data over the specified number of days."""
        cutoff = time.time() - (days * 86400)
        with self._conn() as conn:
            # Total drifts by severity
            severity_rows = conn.execute(
                "SELECT severity, COUNT(*) as cnt FROM drift_records WHERE detected_at > ? GROUP BY severity",
                (cutoff,),
            ).fetchall()
            # Drifts by category
            category_rows = conn.execute(
                "SELECT category, COUNT(*) as cnt FROM drift_records WHERE detected_at > ? GROUP BY category",
                (cutoff,),
            ).fetchall()
            # Resolution rate
            total = conn.execute(
                "SELECT COUNT(*) as cnt FROM drift_records WHERE detected_at > ?",
                (cutoff,),
            ).fetchone()["cnt"]
            resolved = conn.execute(
                "SELECT COUNT(*) as cnt FROM drift_records WHERE detected_at > ? AND resolved_at IS NOT NULL",
                (cutoff,),
            ).fetchone()["cnt"]
        return {
            "period_days": days,
            "total_drifts": total,
            "resolved_drifts": resolved,
            "resolution_rate": round(resolved / total * 100, 1) if total > 0 else 0.0,
            "by_severity": {row["severity"]: row["cnt"] for row in severity_rows},
            "by_category": {row["category"]: row["cnt"] for row in category_rows},
        }
