"""
Abstract base classes for AutoPIL storage backends.

All methods that touch stored data are scoped by tenant_id.
Swap backends by changing the concrete class — nothing else changes.
"""

from abc import ABC, abstractmethod
from datetime import datetime

from autopil.models import AgentAction, AuditEvent, CatalogClassification, CatalogConnection, SyncResult


class AuditLogBase(ABC):

    @abstractmethod
    def record(self, event: AuditEvent, tenant_id: str = "default") -> None:
        """Persist an audit event. Append-only — no updates."""

    @abstractmethod
    def get_session_events(self, session_id: str, tenant_id: str = "default") -> list[AuditEvent]:
        """Return all events for a session scoped to tenant, ordered by timestamp."""

    @abstractmethod
    def get_all_events(self, tenant_id: str = "default") -> list[AuditEvent]:
        """Return all events for a tenant, ordered by timestamp."""

    @abstractmethod
    def get_session_owner(self, session_id: str, tenant_id: str = "default") -> str | None:
        """Return the agent_role that first recorded an event for this session."""

    @abstractmethod
    def verify_chain(self, tenant_id: str = "default") -> dict:
        """
        Walk all events for the tenant in timestamp order and verify the SHA-256
        hash chain is unbroken.

        Returns a dict with:
            valid        bool   — True if every chained event's hash is intact
            total        int    — total events in the tenant
            chained      int    — events that carry a chain_hash
            legacy       int    — events recorded before chaining was enabled
            broken_at    str|None — event_id of the first broken link, or None
        """

    @abstractmethod
    def purge_old_events(self, retention_days: int, tenant_id: str | None = None) -> int:
        """
        Delete audit events older than retention_days and store a chain anchor
        so verify_chain remains valid over the surviving events.

        Pass tenant_id to scope the purge to one tenant; None purges all tenants.
        Returns the total number of events deleted.
        """


class KeyStoreBase(ABC):

    @abstractmethod
    def create_key(
        self,
        name: str,
        tenant_id: str,
        is_superadmin: bool = False,
        scope: str = "admin",
        expires_days: int | None = None,
    ) -> tuple[str, str]:
        """Create a key for a tenant. Returns (key_id, plaintext). Plaintext shown once."""

    @abstractmethod
    def validate(self, plaintext: str) -> dict | None:
        """
        Validate a plaintext key. Returns dict with key_id, tenant_id, is_superadmin, scope,
        or None if invalid/revoked/expired. Updates last_used timestamp.
        """

    @abstractmethod
    def list_keys(self, tenant_id: str) -> list[dict]:
        """Return all keys for a tenant. Never include plaintext or hash."""

    @abstractmethod
    def revoke(self, key_id: str, tenant_id: str) -> bool:
        """Revoke a key. Returns True if the key existed and belonged to this tenant."""

    @abstractmethod
    def rotate_key(self, key_id: str, tenant_id: str) -> tuple[str, str] | None:
        """
        Create a replacement key (same name, scope, expiry duration) then revoke the old one.
        Returns (new_key_id, plaintext) or None if the original key was not found.
        """

    @abstractmethod
    def count(self, tenant_id: str | None = None) -> int:
        """Total keys. Pass tenant_id to count for a specific tenant."""


class TenantStoreBase(ABC):

    @abstractmethod
    def create_tenant(self, name: str) -> str:
        """Create a tenant. Returns tenant_id."""

    @abstractmethod
    def get_tenant(self, tenant_id: str) -> dict | None:
        """Return tenant record or None."""

    @abstractmethod
    def list_tenants(self) -> list[dict]:
        """Return all tenants."""

    @abstractmethod
    def deactivate_tenant(self, tenant_id: str) -> bool:
        """Deactivate a tenant. Returns True if it existed."""

    @abstractmethod
    def count(self) -> int:
        """Total number of tenants."""


class PolicyStoreBase(ABC):

    @abstractmethod
    def upsert(self, policy: dict, tenant_id: str, changed_by: str | None = None) -> str:
        """Create or update a policy. Saves old version to history. Returns policy_id."""

    @abstractmethod
    def get(self, name: str, tenant_id: str) -> dict | None:
        """Return active policy by name, or None."""

    @abstractmethod
    def list_active(self, tenant_id: str) -> list[dict]:
        """Return all active policies for tenant, ordered by name."""

    @abstractmethod
    def delete(self, name: str, tenant_id: str) -> bool:
        """Soft-delete a policy. Returns True if it existed and was active."""

    @abstractmethod
    def get_history(self, name: str, tenant_id: str) -> list[dict]:
        """Return version history for a policy, newest first."""

    @abstractmethod
    def count(self, tenant_id: str) -> int:
        """Count active policies for a tenant."""


class LineageStoreBase(ABC):

    @abstractmethod
    def record_action(self, action: AgentAction, tenant_id: str) -> None:
        """Record an agent action linked to an audit event. Append-only."""

    @abstractmethod
    def get_actions_for_event(self, event_id: str, tenant_id: str) -> list[AgentAction]:
        """Return all actions linked to a specific audit event, ordered by timestamp."""

    @abstractmethod
    def get_actions_for_session(self, session_id: str, tenant_id: str) -> list[AgentAction]:
        """Return all actions for a session, ordered by timestamp."""


class SessionStoreBase(ABC):

    @abstractmethod
    def create_session(
        self,
        session_id: str,
        agent_role: str,
        user_id: str,
        tenant_id: str,
        ttl_minutes: int | None = None,
    ) -> None:
        """Persist a new session record. Idempotent — silently skips if already exists."""

    @abstractmethod
    def get_session(self, session_id: str, tenant_id: str) -> dict | None:
        """Return session record or None if unknown."""

    @abstractmethod
    def touch_session(self, session_id: str, tenant_id: str) -> None:
        """Update last_active to now."""

    @abstractmethod
    def revoke_session(self, session_id: str, tenant_id: str) -> bool:
        """Mark a session as revoked. Returns True if found."""

    @abstractmethod
    def list_sessions(self, tenant_id: str, include_expired: bool = False) -> list[dict]:
        """Return sessions for tenant, newest first. Excludes expired unless requested."""

    @abstractmethod
    def is_valid(self, session_id: str, tenant_id: str) -> tuple[bool, str]:
        """
        Check whether a session may proceed.
        Returns (True, "") if valid, or (False, reason) if expired/revoked/unknown.
        """


class AlertStoreBase(ABC):

    @abstractmethod
    def create_rule(self, rule: dict, tenant_id: str) -> str:
        """Create an alert rule. Returns rule_id."""

    @abstractmethod
    def get_rule(self, rule_id: str, tenant_id: str) -> dict | None:
        """Return rule by ID, or None."""

    @abstractmethod
    def list_rules(self, tenant_id: str) -> list[dict]:
        """Return all rules for tenant, ordered by created_at."""

    @abstractmethod
    def update_rule(self, rule_id: str, updates: dict, tenant_id: str) -> bool:
        """Update mutable fields on a rule. Returns True if found."""

    @abstractmethod
    def delete_rule(self, rule_id: str, tenant_id: str) -> bool:
        """Delete a rule. Returns True if found."""

    @abstractmethod
    def mark_fired(self, rule_id: str, fired_at: str) -> None:
        """Update last_fired_at for cooldown tracking."""

    @abstractmethod
    def record_delivery(self, delivery: dict) -> None:
        """Record a delivery attempt (sent, failed, or no_destination)."""

    @abstractmethod
    def list_deliveries(self, tenant_id: str, limit: int = 50) -> list[dict]:
        """Return recent deliveries for tenant, newest first."""


class CatalogStoreBase(ABC):

    @abstractmethod
    def create_connection(self, conn: CatalogConnection) -> str:
        """Persist a new catalog connection. Returns connection_id."""

    @abstractmethod
    def get_connection(self, connection_id: str, tenant_id: str) -> CatalogConnection | None:
        """Return connection by ID scoped to tenant, or None."""

    @abstractmethod
    def list_connections(self, tenant_id: str) -> list[CatalogConnection]:
        """Return all connections for a tenant, ordered by display_name."""

    @abstractmethod
    def list_all_connections(self) -> list[CatalogConnection]:
        """Return all connections across all tenants (used for key rotation)."""

    @abstractmethod
    def update_connection(self, connection_id: str, updates: dict, tenant_id: str) -> bool:
        """Update mutable fields. Returns True if found and updated."""

    @abstractmethod
    def delete_connection(self, connection_id: str, tenant_id: str) -> bool:
        """Hard-delete a connection and its classifications. Returns True if found."""

    @abstractmethod
    def mark_sync_completed(self, connection_id: str, sync_at: datetime) -> None:
        """Update last_sync_at on a connection after a successful sync."""

    @abstractmethod
    def upsert_classifications(self, classifications: list[CatalogClassification]) -> int:
        """
        Insert or replace classifications by (connection_id, asset_fqn).
        Returns count of rows written.
        """

    @abstractmethod
    def get_classifications_for_asset(
        self, tenant_id: str, asset_fqn: str
    ) -> list[CatalogClassification]:
        """
        Return non-expired classifications for a given asset_fqn.
        Uses prefix matching: 'reports' matches 'catalog.schema.reports'.
        """

    @abstractmethod
    def list_classifications(
        self,
        tenant_id: str,
        connection_id: str | None = None,
        sensitivity_label: str | None = None,
    ) -> list[CatalogClassification]:
        """Return classifications for a tenant, optionally filtered."""

    @abstractmethod
    def delete_stale_classifications(self, connection_id: str) -> int:
        """Remove expired classifications for a connection. Returns count deleted."""

    @abstractmethod
    def record_sync(self, result: SyncResult) -> None:
        """Append a sync history record."""

    @abstractmethod
    def list_sync_history(
        self, connection_id: str, tenant_id: str, limit: int = 25
    ) -> list[SyncResult]:
        """Return sync history for a connection, newest first."""

    @abstractmethod
    def get_catalog_stats(self, tenant_id: str) -> dict:  # noqa: D401
        """
        Return aggregate stats for the catalog tab health panel.

        Keys:
            total_connections          int
            active_connections         int  (status == 'active')
            degraded_connections       int  (status == 'degraded')
            disabled_connections       int  (is_enabled == False)
            total_classifications      int
            sensitivity_distribution   dict[str, int]  label → count
            coverage_by_catalog_type   dict[str, int]  catalog_type → classification count
        """


class TrialStoreBase(ABC):

    @abstractmethod
    def create_trial(
        self, trial_id: str, tenant_id: str, name: str, email: str,
        company: str, industry: str, trial_end: str,
    ) -> None:
        """Persist a new trial record."""

    @abstractmethod
    def get_trial_by_tenant(self, tenant_id: str) -> dict | None:
        """Return trial record for a tenant, or None."""

    @abstractmethod
    def list_active_trials(self) -> list[dict]:
        """Return all trials with status='active'."""

    @abstractmethod
    def list_all_trials(self) -> list[dict]:
        """Return all trials ordered by trial_start descending."""

    @abstractmethod
    def expire_trials(self, before: "datetime") -> list[str]:
        """Mark trials past their trial_end as expired. Returns list of tenant_ids."""

    @abstractmethod
    def mark_email_sent(self, trial_id: str, email_type: str) -> None:
        """Record that a drip email (day7 or day13) was sent for this trial."""
