# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Licensed under the MIT License - see LICENSE file for details

"""
Credential Audit Log — tracks credential lifecycle events.

Provides a lightweight, credential-specific audit trail stored in SQLite.
Complements the general audit system (audit.py) with focused credential
event tracking for security compliance.

Usage:
    from familiar.core.credential_audit import (
        log_credential_event, get_credential_events, CredentialEventType,
    )

    log_credential_event("github", CredentialEventType.VALIDATED, source="startup")
    events = get_credential_events(service_key="github", limit=50)
"""

from __future__ import annotations

import json
import logging
import sqlite3
import threading
import uuid
from contextlib import contextmanager
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)


# =============================================================================
# ENUMS
# =============================================================================


class CredentialEventType(str, Enum):
    """Types of credential lifecycle events."""

    VALIDATED = "validated"
    REFRESHED = "refreshed"
    UPDATED = "updated"
    FAILED = "failed"
    ACCESSED = "accessed"
    INVALIDATED = "invalidated"
    CREATED = "created"
    DELETED = "deleted"
    PREFLIGHT_BLOCKED = "preflight_blocked"
    RETRY_REFRESH = "retry_refresh"


# =============================================================================
# DATA CLASS
# =============================================================================


@dataclass
class CredentialEvent:
    """A single credential audit event."""

    id: str = ""
    service_key: str = ""
    event_type: str = ""
    timestamp: str = ""
    details: str = ""
    source: str = ""

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "service_key": self.service_key,
            "event_type": self.event_type,
            "timestamp": self.timestamp,
            "details": self.details,
            "source": self.source,
        }


# =============================================================================
# AUDIT STORE
# =============================================================================

_DB_PATH = Path.home() / ".familiar" / "data" / "credential_audit.db"
_store_lock = threading.Lock()


@contextmanager
def _get_conn(db_path: Optional[Path] = None):
    """Get a database connection with row factory."""
    path = db_path or _DB_PATH
    conn = sqlite3.connect(str(path))
    conn.row_factory = sqlite3.Row
    try:
        yield conn
        conn.commit()
    finally:
        conn.close()


def _init_db(db_path: Optional[Path] = None):
    """Create the audit table if it doesn't exist."""
    path = db_path or _DB_PATH
    path.parent.mkdir(parents=True, exist_ok=True)
    with _get_conn(path) as conn:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS credential_events (
                id TEXT PRIMARY KEY,
                service_key TEXT NOT NULL,
                event_type TEXT NOT NULL,
                timestamp TEXT NOT NULL,
                details TEXT DEFAULT '',
                source TEXT DEFAULT ''
            );
            CREATE INDEX IF NOT EXISTS idx_cred_event_service
                ON credential_events(service_key);
            CREATE INDEX IF NOT EXISTS idx_cred_event_timestamp
                ON credential_events(timestamp);
            CREATE INDEX IF NOT EXISTS idx_cred_event_type
                ON credential_events(event_type);
        """)


# Track whether DB has been initialized this process
_db_initialized = False


def _ensure_db(db_path: Optional[Path] = None):
    global _db_initialized
    if _db_initialized and db_path is None:
        return
    _init_db(db_path)
    if db_path is None:
        _db_initialized = True


# =============================================================================
# PUBLIC API
# =============================================================================


def log_credential_event(
    service_key: str,
    event_type: CredentialEventType,
    details: str = "",
    source: str = "",
    db_path: Optional[Path] = None,
) -> CredentialEvent:
    """Log a credential lifecycle event.

    Args:
        service_key: The credential service key (e.g. "github", "slack")
        event_type: Type of event (validated, refreshed, failed, etc.)
        details: Human-readable details about what happened
        source: Where the event originated (e.g. "startup", "tool_executor", "dashboard")
        db_path: Override DB path (for testing)

    Returns:
        The logged CredentialEvent
    """
    event = CredentialEvent(
        id=str(uuid.uuid4()),
        service_key=service_key,
        event_type=event_type.value if isinstance(event_type, CredentialEventType) else event_type,
        timestamp=datetime.now(timezone.utc).isoformat(),
        details=details,
        source=source,
    )

    try:
        _ensure_db(db_path)
        with _store_lock:
            with _get_conn(db_path) as conn:
                conn.execute(
                    "INSERT INTO credential_events (id, service_key, event_type, timestamp, details, source) "
                    "VALUES (?, ?, ?, ?, ?, ?)",
                    (
                        event.id,
                        event.service_key,
                        event.event_type,
                        event.timestamp,
                        event.details,
                        event.source,
                    ),
                )
        logger.debug(
            "Credential audit: %s %s (%s) — %s",
            event.event_type,
            event.service_key,
            event.source,
            event.details[:100] if event.details else "",
        )
    except Exception as e:
        logger.warning("Failed to log credential event: %s", e)

    return event


def get_credential_events(
    service_key: Optional[str] = None,
    event_type: Optional[str] = None,
    since: Optional[str] = None,
    limit: int = 100,
    db_path: Optional[Path] = None,
) -> list[CredentialEvent]:
    """Query credential audit events.

    Args:
        service_key: Filter by service key
        event_type: Filter by event type
        since: ISO timestamp — return events after this time
        limit: Max results (default 100)
        db_path: Override DB path (for testing)

    Returns:
        List of CredentialEvent, newest first
    """
    try:
        _ensure_db(db_path)
        conditions = []
        params = []

        if service_key:
            conditions.append("service_key = ?")
            params.append(service_key)
        if event_type:
            ev = event_type.value if isinstance(event_type, CredentialEventType) else event_type
            conditions.append("event_type = ?")
            params.append(ev)
        if since:
            conditions.append("timestamp > ?")
            params.append(since)

        where = " WHERE " + " AND ".join(conditions) if conditions else ""
        query = f"SELECT * FROM credential_events{where} ORDER BY timestamp DESC LIMIT ?"
        params.append(limit)

        with _get_conn(db_path) as conn:
            rows = conn.execute(query, params).fetchall()

        return [
            CredentialEvent(
                id=row["id"],
                service_key=row["service_key"],
                event_type=row["event_type"],
                timestamp=row["timestamp"],
                details=row["details"],
                source=row["source"],
            )
            for row in rows
        ]
    except Exception as e:
        logger.warning("Failed to query credential events: %s", e)
        return []


def get_event_summary(
    service_key: Optional[str] = None,
    db_path: Optional[Path] = None,
) -> dict:
    """Get a summary of credential events.

    Returns:
        Dict with total count, counts by event_type, and most recent event per service.
    """
    try:
        _ensure_db(db_path)
        with _get_conn(db_path) as conn:
            # Total count
            if service_key:
                total = conn.execute(
                    "SELECT COUNT(*) FROM credential_events WHERE service_key = ?",
                    (service_key,),
                ).fetchone()[0]
            else:
                total = conn.execute("SELECT COUNT(*) FROM credential_events").fetchone()[0]

            # Counts by event type
            if service_key:
                type_rows = conn.execute(
                    "SELECT event_type, COUNT(*) as cnt FROM credential_events "
                    "WHERE service_key = ? GROUP BY event_type",
                    (service_key,),
                ).fetchall()
            else:
                type_rows = conn.execute(
                    "SELECT event_type, COUNT(*) as cnt FROM credential_events GROUP BY event_type",
                ).fetchall()

            by_type = {row["event_type"]: row["cnt"] for row in type_rows}

        return {"total": total, "by_type": by_type}
    except Exception as e:
        logger.warning("Failed to get credential event summary: %s", e)
        return {"total": 0, "by_type": {}}


# =============================================================================
# SKILL CREDENTIAL MAP (Per-Skill Scoping)
# =============================================================================

# Maps skill names to the credential service_keys they are permitted to access.
# Used by the tool executor to enforce least-privilege: a skill can only trigger
# credential checks for services it declares.
SKILL_CREDENTIAL_MAP: dict[str, list[str]] = {
    "github": ["github"],
    "email": ["email"],
    "calendar": ["google_workspace"],
    "gmail": ["google_workspace"],
    "gdrive": ["google_workspace"],
    "contacts": ["google_workspace"],
    "slack": ["slack"],
    "hubspot": ["hubspot"],
    "todoist": ["todoist"],
    "stripe": ["stripe"],
    "square": ["square"],
    "reddit": ["reddit"],
    "brave_search": ["brave_search"],
    "quickbooks": ["quickbooks"],
    "sms": ["sms"],
    "mastodon": ["mastodon"],
    "bluesky": ["bluesky"],
    "instagram": ["instagram"],
    "telegram": ["telegram"],
    "discord": ["discord"],
    "jellyfin": ["jellyfin"],
    "gitea": ["gitea"],
    "homeassistant": ["homeassistant"],
    "joplin": ["joplin"],
    "pihole": ["pihole"],
    "nextcloud": ["nextcloud"],
    "mealie": ["mealie"],
    "clockify": ["clockify"],
    "etsy": ["etsy"],
    "mailerlite": ["mailerlite"],
    "shippo": ["shippo"],
    "calcom": ["calcom"],
    "searxng": ["searxng"],
    "vaultwarden": ["vaultwarden"],
    "documents": [],
    "charts": [],
    "web_search": ["brave_search"],
    "browser": ["browser"],
}


def get_skill_credentials(skill_name: str) -> list[str]:
    """Return the credential service_keys a skill is permitted to access.

    If the skill is not in the map, returns an empty list (no credential
    access permitted).
    """
    return SKILL_CREDENTIAL_MAP.get(skill_name, [])


def check_skill_credential_access(skill_name: str, service_key: str) -> bool:
    """Check if a skill is permitted to access a given credential.

    Returns True if:
    - The skill explicitly declares the service_key in SKILL_CREDENTIAL_MAP
    - The skill is not in the map (unknown skills get no credential access,
      but we don't block — the tool executor's TOOL_CREDENTIAL_MAP handles
      which credentials are actually needed)
    """
    allowed = SKILL_CREDENTIAL_MAP.get(skill_name)
    if allowed is None:
        # Unknown skill — defer to TOOL_CREDENTIAL_MAP in credential_health
        return True
    return service_key in allowed


# =============================================================================
# TEST HELPERS
# =============================================================================


def _reset_for_testing():
    """Reset module state for test isolation."""
    global _db_initialized
    _db_initialized = False
