"""SQLite-backed credential store with encryption at rest and audit logging."""
from __future__ import annotations

import json
import logging
import os
import secrets
import sqlite3
import threading
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Dict, List, Optional

from mesh.vault.crypto import VaultCrypto
from mesh.vault.models import (
    CloudProvider,
    CredentialEntry,
    CredentialGrant,
    CredentialRequest,
    CredentialScope,
    VaultStatus,
)

logger = logging.getLogger(__name__)

_BUSY_RETRIES = 3
_BUSY_DELAY_S = 0.1


class VaultStore:
    """Encrypted credential store backed by SQLite.

    Parameters
    ----------
    db_path:
        Path to the SQLite database file.  Created with ``0o600`` permissions.
    crypto:
        :class:`VaultCrypto` instance used to encrypt/decrypt credential fields.
    grant_ttl_minutes:
        Default time-to-live for credential grants (default 15 min).
    """

    def __init__(
        self,
        db_path: str,
        crypto: VaultCrypto,
        grant_ttl_minutes: int = 15,
    ) -> None:
        self.db_path = Path(db_path)
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        self._crypto = crypto
        self._grant_ttl = grant_ttl_minutes
        self._write_lock = threading.Lock()
        self._conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
        self._conn.row_factory = sqlite3.Row
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._conn.execute("PRAGMA busy_timeout=5000")
        self._create_tables()
        # Restrict file permissions (owner read/write only)
        try:
            os.chmod(str(self.db_path), 0o600)
        except OSError:
            logger.warning("Could not set vault DB permissions to 0600")

    # ── internal helpers ──────────────────────────────────────────

    def _execute_write(self, fn):
        with self._write_lock:
            for attempt in range(_BUSY_RETRIES):
                try:
                    return fn()
                except sqlite3.OperationalError as e:
                    if "database is locked" in str(e) and attempt < _BUSY_RETRIES - 1:
                        logger.debug("SQLite busy, retrying (%d/%d)", attempt + 1, _BUSY_RETRIES)
                        time.sleep(_BUSY_DELAY_S)
                    else:
                        raise

    def _now_iso(self) -> str:
        return datetime.now(timezone.utc).isoformat()

    def _create_tables(self):
        def _do():
            self._conn.executescript("""
                CREATE TABLE IF NOT EXISTS vault_credentials (
                    credential_id TEXT PRIMARY KEY,
                    name TEXT NOT NULL,
                    provider TEXT NOT NULL,
                    service TEXT NOT NULL,
                    scope TEXT NOT NULL,
                    allowed_nodes_json TEXT,
                    fields_json TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL,
                    last_accessed TEXT,
                    access_count INTEGER DEFAULT 0,
                    expires_at TEXT
                );

                CREATE TABLE IF NOT EXISTS vault_access_log (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    timestamp TEXT NOT NULL,
                    credential_id TEXT NOT NULL,
                    node_id TEXT,
                    action TEXT NOT NULL,
                    granted INTEGER NOT NULL,
                    ip_address TEXT
                );

                CREATE INDEX IF NOT EXISTS idx_vault_log_cred
                    ON vault_access_log(credential_id, timestamp);
                CREATE INDEX IF NOT EXISTS idx_vault_cred_provider
                    ON vault_credentials(provider);
            """)
            self._conn.commit()
        self._execute_write(_do)

    def _log_access(
        self,
        credential_id: str,
        node_id: str | None,
        action: str,
        granted: bool,
        ip_address: str | None = None,
    ) -> None:
        def _do():
            self._conn.execute(
                "INSERT INTO vault_access_log "
                "(timestamp, credential_id, node_id, action, granted, ip_address) "
                "VALUES (?, ?, ?, ?, ?, ?)",
                (self._now_iso(), credential_id, node_id, action, int(granted), ip_address),
            )
            self._conn.commit()
        self._execute_write(_do)

    def close(self) -> None:
        try:
            self._conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
            self._conn.close()
            logger.info("VaultStore closed: %s", self.db_path)
        except Exception as e:
            logger.warning("VaultStore close error: %s", e)

    # ── CRUD operations ───────────────────────────────────────────

    def store(self, entry: CredentialRequest) -> str:
        """Encrypt and store a credential.  Returns the new ``credential_id``."""
        credential_id = secrets.token_hex(16)
        now = self._now_iso()

        # Encrypt each field value individually
        encrypted_fields: Dict[str, str] = {}
        for k, v in entry.fields.items():
            encrypted_fields[k] = self._crypto.encrypt(v)

        def _do():
            self._conn.execute(
                "INSERT INTO vault_credentials "
                "(credential_id, name, provider, service, scope, allowed_nodes_json, "
                "fields_json, created_at, updated_at, expires_at) "
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                (
                    credential_id,
                    entry.name,
                    entry.provider.value,
                    entry.service.value,
                    entry.scope.value,
                    json.dumps(entry.allowed_nodes),
                    json.dumps(encrypted_fields),
                    now,
                    now,
                    entry.expires_at.isoformat() if entry.expires_at else None,
                ),
            )
            self._conn.commit()
        self._execute_write(_do)
        self._log_access(credential_id, None, "store", True)
        logger.info("Stored credential %s (%s/%s)", credential_id, entry.provider.value, entry.service.value)
        return credential_id

    def retrieve(
        self,
        credential_id: str,
        requesting_node_id: str,
        ip_address: str | None = None,
    ) -> Optional[CredentialGrant]:
        """Retrieve a credential as a time-limited grant.

        Returns ``None`` if the credential does not exist or the requesting
        node is not authorised.
        """
        row = self._conn.execute(
            "SELECT * FROM vault_credentials WHERE credential_id = ?",
            (credential_id,),
        ).fetchone()
        if row is None:
            self._log_access(credential_id, requesting_node_id, "retrieve", False, ip_address)
            return None

        row_dict = dict(row)

        # Check expiry
        if row_dict.get("expires_at"):
            exp = datetime.fromisoformat(row_dict["expires_at"])
            if datetime.now(timezone.utc) > exp.replace(tzinfo=timezone.utc) if exp.tzinfo is None else exp:
                self._log_access(credential_id, requesting_node_id, "retrieve_expired", False, ip_address)
                return None

        # Authorization check
        if not self._check_node_access(row_dict, requesting_node_id):
            self._log_access(credential_id, requesting_node_id, "retrieve_denied", False, ip_address)
            logger.warning(
                "Access denied: node %s requested credential %s (scope=%s)",
                requesting_node_id, credential_id, row_dict["scope"],
            )
            return None

        # Decrypt fields
        encrypted_fields = json.loads(row_dict["fields_json"])
        decrypted: Dict[str, str] = {}
        for k, v in encrypted_fields.items():
            decrypted[k] = self._crypto.decrypt(v)

        # Update access metadata
        now = self._now_iso()
        def _do():
            self._conn.execute(
                "UPDATE vault_credentials SET last_accessed = ?, access_count = access_count + 1 "
                "WHERE credential_id = ?",
                (now, credential_id),
            )
            self._conn.commit()
        self._execute_write(_do)

        self._log_access(credential_id, requesting_node_id, "retrieve", True, ip_address)

        grant_expires = datetime.now(timezone.utc) + timedelta(minutes=self._grant_ttl)
        return CredentialGrant(
            grant_id=secrets.token_hex(16),
            credential_id=credential_id,
            provider=CloudProvider(row_dict["provider"]),
            fields=decrypted,
            expires_at=grant_expires,
        )

    def list_credentials(self) -> List[CredentialEntry]:
        """List all credentials (metadata only -- fields are NOT decrypted)."""
        rows = self._conn.execute(
            "SELECT * FROM vault_credentials ORDER BY name"
        ).fetchall()
        result: List[CredentialEntry] = []
        for row in rows:
            d = dict(row)
            result.append(CredentialEntry(
                credential_id=d["credential_id"],
                name=d["name"],
                provider=CloudProvider(d["provider"]),
                service=d["service"],
                scope=CredentialScope(d["scope"]),
                allowed_nodes=json.loads(d["allowed_nodes_json"]) if d.get("allowed_nodes_json") else [],
                fields={},  # Never expose encrypted fields in listings
                created_at=datetime.fromisoformat(d["created_at"]),
                updated_at=datetime.fromisoformat(d["updated_at"]),
                last_accessed=datetime.fromisoformat(d["last_accessed"]) if d.get("last_accessed") else None,
                access_count=d.get("access_count", 0),
                expires_at=datetime.fromisoformat(d["expires_at"]) if d.get("expires_at") else None,
            ))
        return result

    def delete(self, credential_id: str) -> bool:
        """Delete a credential.  Returns True if it existed."""
        def _do():
            cursor = self._conn.execute(
                "DELETE FROM vault_credentials WHERE credential_id = ?",
                (credential_id,),
            )
            self._conn.commit()
            return cursor.rowcount > 0
        deleted = self._execute_write(_do)
        if deleted:
            self._log_access(credential_id, None, "delete", True)
            logger.info("Deleted credential %s", credential_id)
        return bool(deleted)

    def rotate(self, credential_id: str, new_fields: Dict[str, str]) -> bool:
        """Replace credential field values with new ones (re-encrypted).

        Returns True if the credential existed.
        """
        encrypted: Dict[str, str] = {}
        for k, v in new_fields.items():
            encrypted[k] = self._crypto.encrypt(v)

        now = self._now_iso()

        def _do():
            cursor = self._conn.execute(
                "UPDATE vault_credentials SET fields_json = ?, updated_at = ? "
                "WHERE credential_id = ?",
                (json.dumps(encrypted), now, credential_id),
            )
            self._conn.commit()
            return cursor.rowcount > 0
        updated = self._execute_write(_do)
        if updated:
            self._log_access(credential_id, None, "rotate", True)
            logger.info("Rotated credential %s", credential_id)
        return bool(updated)

    # ── access control ────────────────────────────────────────────

    def check_access(self, credential_id: str, node_id: str) -> bool:
        """Check whether *node_id* is authorised to access *credential_id*."""
        row = self._conn.execute(
            "SELECT scope, allowed_nodes_json FROM vault_credentials WHERE credential_id = ?",
            (credential_id,),
        ).fetchone()
        if row is None:
            return False
        return self._check_node_access(dict(row), node_id)

    def grant_node(self, credential_id: str, node_id: str) -> bool:
        """Add *node_id* to the allowed list for a credential."""
        row = self._conn.execute(
            "SELECT allowed_nodes_json FROM vault_credentials WHERE credential_id = ?",
            (credential_id,),
        ).fetchone()
        if row is None:
            return False
        nodes = json.loads(row["allowed_nodes_json"]) if row["allowed_nodes_json"] else []
        if node_id not in nodes:
            nodes.append(node_id)
        def _do():
            self._conn.execute(
                "UPDATE vault_credentials SET allowed_nodes_json = ?, updated_at = ? "
                "WHERE credential_id = ?",
                (json.dumps(nodes), self._now_iso(), credential_id),
            )
            self._conn.commit()
        self._execute_write(_do)
        self._log_access(credential_id, node_id, "grant", True)
        return True

    def revoke_node(self, credential_id: str, node_id: str) -> bool:
        """Remove *node_id* from the allowed list for a credential."""
        row = self._conn.execute(
            "SELECT allowed_nodes_json FROM vault_credentials WHERE credential_id = ?",
            (credential_id,),
        ).fetchone()
        if row is None:
            return False
        nodes = json.loads(row["allowed_nodes_json"]) if row["allowed_nodes_json"] else []
        if node_id in nodes:
            nodes.remove(node_id)
        def _do():
            self._conn.execute(
                "UPDATE vault_credentials SET allowed_nodes_json = ?, updated_at = ? "
                "WHERE credential_id = ?",
                (json.dumps(nodes), self._now_iso(), credential_id),
            )
            self._conn.commit()
        self._execute_write(_do)
        self._log_access(credential_id, node_id, "revoke", True)
        return True

    def _check_node_access(self, row_dict: dict, node_id: str) -> bool:
        scope = row_dict.get("scope", "")
        if scope == CredentialScope.GLOBAL.value:
            return True
        if scope == CredentialScope.CONTROLLER.value:
            return node_id == "__controller__"
        if scope == CredentialScope.NODE.value:
            allowed = json.loads(row_dict.get("allowed_nodes_json") or "[]")
            return node_id in allowed
        if scope == CredentialScope.SERVICE.value:
            # Service scope -- any authenticated node may access
            return True
        return False

    # ── audit log ─────────────────────────────────────────────────

    def get_access_log(
        self,
        credential_id: str | None = None,
        limit: int = 50,
    ) -> List[Dict]:
        """Return the access audit log, optionally filtered by credential."""
        if credential_id:
            rows = self._conn.execute(
                "SELECT * FROM vault_access_log WHERE credential_id = ? "
                "ORDER BY timestamp DESC LIMIT ?",
                (credential_id, limit),
            ).fetchall()
        else:
            rows = self._conn.execute(
                "SELECT * FROM vault_access_log ORDER BY timestamp DESC LIMIT ?",
                (limit,),
            ).fetchall()
        return [dict(r) for r in rows]

    # ── status ────────────────────────────────────────────────────

    def status(self) -> VaultStatus:
        """Return current vault status (counts, providers, last access)."""
        rows = self._conn.execute(
            "SELECT provider, COUNT(*) as cnt FROM vault_credentials GROUP BY provider"
        ).fetchall()
        providers = {r["provider"]: r["cnt"] for r in rows}
        total = sum(providers.values())

        last_row = self._conn.execute(
            "SELECT last_accessed FROM vault_credentials "
            "WHERE last_accessed IS NOT NULL ORDER BY last_accessed DESC LIMIT 1"
        ).fetchone()
        last_access = None
        if last_row and last_row["last_accessed"]:
            last_access = datetime.fromisoformat(last_row["last_accessed"])

        return VaultStatus(
            sealed=False,  # If we can query, we are unsealed
            credential_count=total,
            providers=providers,
            last_access=last_access,
        )

    # ── export / import ───────────────────────────────────────────

    def export_encrypted(self) -> List[Dict]:
        """Export all credentials as a list of dicts (fields remain encrypted)."""
        rows = self._conn.execute("SELECT * FROM vault_credentials").fetchall()
        return [dict(r) for r in rows]

    def import_encrypted(self, entries: List[Dict]) -> int:
        """Import credentials from an encrypted export.  Returns count imported."""
        count = 0
        for entry in entries:
            def _do(e=entry):
                self._conn.execute(
                    "INSERT OR REPLACE INTO vault_credentials "
                    "(credential_id, name, provider, service, scope, allowed_nodes_json, "
                    "fields_json, created_at, updated_at, last_accessed, access_count, expires_at) "
                    "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                    (
                        e["credential_id"], e["name"], e["provider"], e["service"],
                        e["scope"], e.get("allowed_nodes_json"), e["fields_json"],
                        e["created_at"], e["updated_at"], e.get("last_accessed"),
                        e.get("access_count", 0), e.get("expires_at"),
                    ),
                )
                self._conn.commit()
            self._execute_write(_do)
            count += 1
        logger.info("Imported %d credentials", count)
        return count
