"""Identity graph persistence for cross-system record linking."""

from __future__ import annotations

import hashlib
import sqlite3
import uuid
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Iterator

_IDENTITY_SCHEMA_VERSION = 1
_VERSION_KEY = "identity_schema_version"


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


def _normalize_text(value: str | None) -> str:
    return str(value or "").strip()


def _normalize_email(value: str | None) -> str:
    return _normalize_text(value).lower()


def _normalize_domain(value: str | None) -> str:
    normalized = _normalize_text(value).lower()
    if "@" in normalized:
        return normalized.split("@", 1)[1].strip()
    return normalized


def _none_if_blank(value: str) -> str | None:
    return value or None


def _row_to_dict(row: sqlite3.Row | None) -> dict[str, Any] | None:
    if row is None:
        return None
    return {key: row[key] for key in row.keys()}


class IdentityGraph:
    """SQLite-backed identity graph for canonical entities and source links."""

    def __init__(self, db_path: Path | str):
        self.db_path = Path(db_path).resolve()
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        self._init_db()

    @contextmanager
    def _connect(self) -> Iterator[sqlite3.Connection]:
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA busy_timeout=5000")
        conn.execute("PRAGMA foreign_keys=ON")
        try:
            yield conn
            conn.commit()
        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()

    def _init_db(self) -> None:
        with self._connect() as conn:
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS _meta (
                    key TEXT PRIMARY KEY,
                    value TEXT NOT NULL,
                    updated_at TEXT NOT NULL
                )
                """
            )
            current = self._read_version(conn)
            while current < _IDENTITY_SCHEMA_VERSION:
                if current == 0:
                    self._migrate_v1(conn)
                    current = 1
                else:
                    raise ValueError(f"Unsupported identity schema version: {current}")
            self._write_version(conn, current)

    def _read_version(self, conn: sqlite3.Connection) -> int:
        row = conn.execute(
            "SELECT value FROM _meta WHERE key = ?",
            (_VERSION_KEY,),
        ).fetchone()
        if not row:
            return 0
        try:
            return int(row["value"])
        except Exception:
            return 0

    def _write_version(self, conn: sqlite3.Connection, version: int) -> None:
        now = _utc_now_iso()
        conn.execute(
            """
            INSERT INTO _meta(key, value, updated_at)
            VALUES (?, ?, ?)
            ON CONFLICT(key) DO UPDATE SET
                value = excluded.value,
                updated_at = excluded.updated_at
            """,
            (_VERSION_KEY, str(version), now),
        )

    def _migrate_v1(self, conn: sqlite3.Connection) -> None:
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS entities (
                entity_id TEXT PRIMARY KEY,
                entity_type TEXT NOT NULL,
                canonical_name TEXT,
                canonical_domain TEXT,
                canonical_email TEXT,
                created_at TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
            """
        )
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(entity_type)"
        )
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_entities_domain ON entities(canonical_domain)"
        )
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_entities_email ON entities(canonical_email)"
        )

        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS entity_links (
                link_id TEXT PRIMARY KEY,
                entity_id TEXT NOT NULL REFERENCES entities(entity_id) ON DELETE CASCADE,
                source_system TEXT NOT NULL,
                source_object TEXT NOT NULL,
                source_record_id TEXT NOT NULL,
                source_org_id TEXT NOT NULL DEFAULT '',
                confidence REAL NOT NULL DEFAULT 1.0,
                match_method TEXT,
                linked_at TEXT NOT NULL,
                linked_by TEXT,
                UNIQUE(source_system, source_record_id, source_org_id)
            )
            """
        )
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_links_entity ON entity_links(entity_id)"
        )
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_links_source ON entity_links(source_system, source_record_id)"
        )

    def find_entity_by_record(
        self,
        source_system: str,
        source_record_id: str,
        source_org_id: str | None = None,
    ) -> dict[str, Any] | None:
        normalized_system = _normalize_text(source_system).lower()
        normalized_record = _normalize_text(source_record_id)
        normalized_org = _normalize_text(source_org_id).lower()
        if not normalized_system or not normalized_record:
            return None

        with self._connect() as conn:
            row = conn.execute(
                """
                SELECT e.*
                FROM entity_links AS l
                JOIN entities AS e ON e.entity_id = l.entity_id
                WHERE
                    l.source_system = ?
                    AND l.source_record_id = ?
                    AND l.source_org_id = ?
                LIMIT 1
                """,
                (normalized_system, normalized_record, normalized_org),
            ).fetchone()
            return _row_to_dict(row)

    def find_entity_by_email(self, email: str) -> dict[str, Any] | None:
        normalized_email = _normalize_email(email)
        if not normalized_email:
            return None
        with self._connect() as conn:
            row = conn.execute(
                "SELECT * FROM entities WHERE canonical_email = ? LIMIT 1",
                (normalized_email,),
            ).fetchone()
            return _row_to_dict(row)

    def find_entity_by_domain(self, domain: str) -> dict[str, Any] | None:
        normalized_domain = _normalize_domain(domain)
        if not normalized_domain:
            return None
        with self._connect() as conn:
            row = conn.execute(
                "SELECT * FROM entities WHERE canonical_domain = ? LIMIT 1",
                (normalized_domain,),
            ).fetchone()
            return _row_to_dict(row)

    def get_linked_records(self, entity_id: str) -> list[dict[str, Any]]:
        normalized_entity_id = _normalize_text(entity_id)
        if not normalized_entity_id:
            return []

        with self._connect() as conn:
            rows = conn.execute(
                """
                SELECT
                    link_id,
                    entity_id,
                    source_system,
                    source_object,
                    source_record_id,
                    source_org_id,
                    confidence,
                    match_method,
                    linked_at,
                    linked_by
                FROM entity_links
                WHERE entity_id = ?
                ORDER BY linked_at DESC
                """,
                (normalized_entity_id,),
            ).fetchall()
            return [_row_to_dict(row) or {} for row in rows]

    def cross_system_lookup(
        self,
        source_system: str,
        source_record_id: str,
        source_org_id: str | None = None,
    ) -> dict[str, list[dict[str, Any]]]:
        entity = self.find_entity_by_record(source_system, source_record_id, source_org_id)
        if not entity:
            return {}

        linked = self.get_linked_records(str(entity.get("entity_id") or ""))
        grouped: dict[str, list[dict[str, Any]]] = {}
        for record in linked:
            key = str(record.get("source_system") or "").strip().lower() or "unknown"
            grouped.setdefault(key, []).append(record)
        return grouped

    def link_record(
        self,
        *,
        source_system: str,
        source_object: str,
        source_record_id: str,
        source_org_id: str | None = None,
        entity_id: str | None = None,
        entity_type: str = "person",
        canonical_name: str | None = None,
        canonical_domain: str | None = None,
        canonical_email: str | None = None,
        confidence: float = 1.0,
        match_method: str | None = None,
        linked_by: str | None = None,
    ) -> str:
        normalized_system = _normalize_text(source_system).lower()
        normalized_object = _normalize_text(source_object)
        normalized_record = _normalize_text(source_record_id)
        normalized_org = _normalize_text(source_org_id).lower()
        if not normalized_system or not normalized_object or not normalized_record:
            raise ValueError("source_system, source_object, and source_record_id are required")

        normalized_email = _normalize_email(canonical_email)
        normalized_domain = _normalize_domain(canonical_domain) or _normalize_domain(normalized_email)
        normalized_name = _normalize_text(canonical_name)
        normalized_entity_type = _normalize_text(entity_type).lower() or "person"
        normalized_linked_by = _normalize_text(linked_by)
        normalized_match_method = _normalize_text(match_method)
        normalized_entity_id = _normalize_text(entity_id)

        try:
            confidence_value = float(confidence)
        except Exception:
            confidence_value = 1.0
        confidence_value = min(max(confidence_value, 0.0), 1.0)

        now = _utc_now_iso()
        with self._connect() as conn:
            existing_link = conn.execute(
                """
                SELECT entity_id
                FROM entity_links
                WHERE source_system = ? AND source_record_id = ? AND source_org_id = ?
                LIMIT 1
                """,
                (normalized_system, normalized_record, normalized_org),
            ).fetchone()
            if existing_link and not normalized_entity_id:
                normalized_entity_id = str(existing_link["entity_id"])

            if not normalized_entity_id and normalized_email:
                by_email = conn.execute(
                    "SELECT entity_id FROM entities WHERE canonical_email = ? LIMIT 1",
                    (normalized_email,),
                ).fetchone()
                if by_email:
                    normalized_entity_id = str(by_email["entity_id"])

            if not normalized_entity_id and normalized_domain:
                by_domain = conn.execute(
                    "SELECT entity_id FROM entities WHERE canonical_domain = ? LIMIT 1",
                    (normalized_domain,),
                ).fetchone()
                if by_domain:
                    normalized_entity_id = str(by_domain["entity_id"])

            if not normalized_entity_id:
                normalized_entity_id = str(uuid.uuid4())

            conn.execute(
                """
                INSERT INTO entities (
                    entity_id,
                    entity_type,
                    canonical_name,
                    canonical_domain,
                    canonical_email,
                    created_at,
                    updated_at
                )
                VALUES (?, ?, ?, ?, ?, ?, ?)
                ON CONFLICT(entity_id) DO UPDATE SET
                    entity_type = COALESCE(NULLIF(excluded.entity_type, ''), entities.entity_type),
                    canonical_name = COALESCE(NULLIF(excluded.canonical_name, ''), entities.canonical_name),
                    canonical_domain = COALESCE(NULLIF(excluded.canonical_domain, ''), entities.canonical_domain),
                    canonical_email = COALESCE(NULLIF(excluded.canonical_email, ''), entities.canonical_email),
                    updated_at = excluded.updated_at
                """,
                (
                    normalized_entity_id,
                    normalized_entity_type,
                    _none_if_blank(normalized_name),
                    _none_if_blank(normalized_domain),
                    _none_if_blank(normalized_email),
                    now,
                    now,
                ),
            )

            link_id = self._build_link_id(
                normalized_system,
                normalized_record,
                normalized_org,
            )
            conn.execute(
                """
                INSERT INTO entity_links (
                    link_id,
                    entity_id,
                    source_system,
                    source_object,
                    source_record_id,
                    source_org_id,
                    confidence,
                    match_method,
                    linked_at,
                    linked_by
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                ON CONFLICT(source_system, source_record_id, source_org_id) DO UPDATE SET
                    entity_id = excluded.entity_id,
                    source_object = excluded.source_object,
                    confidence = excluded.confidence,
                    match_method = excluded.match_method,
                    linked_at = excluded.linked_at,
                    linked_by = excluded.linked_by
                """,
                (
                    link_id,
                    normalized_entity_id,
                    normalized_system,
                    normalized_object,
                    normalized_record,
                    normalized_org,
                    confidence_value,
                    _none_if_blank(normalized_match_method),
                    now,
                    _none_if_blank(normalized_linked_by),
                ),
            )

        return normalized_entity_id

    def unlink_record(
        self,
        source_system: str,
        source_record_id: str,
        source_org_id: str | None = None,
    ) -> bool:
        normalized_system = _normalize_text(source_system).lower()
        normalized_record = _normalize_text(source_record_id)
        normalized_org = _normalize_text(source_org_id).lower()
        if not normalized_system or not normalized_record:
            return False

        with self._connect() as conn:
            cursor = conn.execute(
                """
                DELETE FROM entity_links
                WHERE source_system = ? AND source_record_id = ? AND source_org_id = ?
                """,
                (normalized_system, normalized_record, normalized_org),
            )
            return int(cursor.rowcount or 0) > 0

    def purge_source_system(self, source_system: str) -> dict[str, int]:
        normalized_system = _normalize_text(source_system).lower()
        if not normalized_system:
            return {"links_deleted": 0, "entities_deleted": 0}

        with self._connect() as conn:
            rows = conn.execute(
                "SELECT DISTINCT entity_id FROM entity_links WHERE source_system = ?",
                (normalized_system,),
            ).fetchall()
            affected_entity_ids = [
                str(row["entity_id"])
                for row in rows
                if row is not None and str(row["entity_id"] or "").strip()
            ]

            links_cursor = conn.execute(
                "DELETE FROM entity_links WHERE source_system = ?",
                (normalized_system,),
            )
            links_deleted = int(links_cursor.rowcount or 0)

            entities_deleted = 0
            if affected_entity_ids:
                placeholders = ", ".join(["?"] * len(affected_entity_ids))
                entities_cursor = conn.execute(
                    f"""
                    DELETE FROM entities
                    WHERE
                        entity_id IN ({placeholders})
                        AND entity_id NOT IN (SELECT DISTINCT entity_id FROM entity_links)
                    """,
                    tuple(affected_entity_ids),
                )
                entities_deleted = int(entities_cursor.rowcount or 0)

            return {
                "links_deleted": links_deleted,
                "entities_deleted": entities_deleted,
            }

    def stats(self) -> dict[str, int]:
        with self._connect() as conn:
            entities = conn.execute("SELECT COUNT(1) AS count FROM entities").fetchone()
            links = conn.execute("SELECT COUNT(1) AS count FROM entity_links").fetchone()
            return {
                "entities": int(entities["count"]) if entities is not None else 0,
                "entity_links": int(links["count"]) if links is not None else 0,
            }

    def purge_unlinked(self, older_than_days: int = 180) -> int:
        days = max(int(older_than_days), 0)
        cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
        with self._connect() as conn:
            cursor = conn.execute(
                """
                DELETE FROM entities
                WHERE
                    updated_at < ?
                    AND entity_id NOT IN (SELECT DISTINCT entity_id FROM entity_links)
                """,
                (cutoff,),
            )
            return int(cursor.rowcount or 0)

    @staticmethod
    def _build_link_id(source_system: str, source_record_id: str, source_org_id: str) -> str:
        base = f"{source_system}:{source_record_id}:{source_org_id}"
        return hashlib.sha256(base.encode("utf-8")).hexdigest()


__all__ = ["IdentityGraph"]
