"""SQLite-backed Indicator of Compromise (IOC) database for AYN Antivirus.

Stores malicious IPs, domains, and URLs sourced from threat-intelligence
feeds so that the network scanner and detectors can perform real-time
lookups.
"""

from __future__ import annotations

import logging
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple

from ayn_antivirus.constants import DEFAULT_DB_PATH

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Schema
# ---------------------------------------------------------------------------
_SCHEMA = """
CREATE TABLE IF NOT EXISTS ioc_ips (
    ip          TEXT PRIMARY KEY,
    threat_name TEXT NOT NULL DEFAULT '',
    type        TEXT NOT NULL DEFAULT 'C2',
    source      TEXT NOT NULL DEFAULT '',
    added_date  TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_ioc_ips_source ON ioc_ips(source);

CREATE TABLE IF NOT EXISTS ioc_domains (
    domain      TEXT PRIMARY KEY,
    threat_name TEXT NOT NULL DEFAULT '',
    type        TEXT NOT NULL DEFAULT 'C2',
    source      TEXT NOT NULL DEFAULT '',
    added_date  TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_ioc_domains_source ON ioc_domains(source);

CREATE TABLE IF NOT EXISTS ioc_urls (
    url         TEXT PRIMARY KEY,
    threat_name TEXT NOT NULL DEFAULT '',
    type        TEXT NOT NULL DEFAULT 'malware_distribution',
    source      TEXT NOT NULL DEFAULT '',
    added_date  TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_ioc_urls_source ON ioc_urls(source);
"""


class IOCDatabase:
    """Manage a local SQLite store of Indicators of Compromise.

    Parameters
    ----------
    db_path:
        Path to the SQLite file.  Shares the same file as
        :class:`HashDatabase` by default; each uses its own tables.
    """

    _VALID_TABLES: frozenset = frozenset({"ioc_ips", "ioc_domains", "ioc_urls"})

    def __init__(self, db_path: str | Path = DEFAULT_DB_PATH) -> None:
        self.db_path = Path(db_path)
        self._conn: Optional[sqlite3.Connection] = None

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def initialize(self) -> None:
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        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.executescript(_SCHEMA)
        self._conn.commit()
        logger.info(
            "IOCDatabase opened: %s (IPs=%d, domains=%d, URLs=%d)",
            self.db_path,
            self._count("ioc_ips"),
            self._count("ioc_domains"),
            self._count("ioc_urls"),
        )

    def close(self) -> None:
        if self._conn:
            self._conn.close()
            self._conn = None

    @property
    def conn(self) -> sqlite3.Connection:
        if self._conn is None:
            self.initialize()
        assert self._conn is not None
        return self._conn

    def _count(self, table: str) -> int:
        if table not in self._VALID_TABLES:
            raise ValueError(f"Invalid table name: {table}")
        return self.conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]

    # ------------------------------------------------------------------
    # IPs
    # ------------------------------------------------------------------

    def add_ip(
        self,
        ip: str,
        threat_name: str = "",
        type: str = "C2",
        source: str = "",
    ) -> None:
        self.conn.execute(
            "INSERT OR REPLACE INTO ioc_ips (ip, threat_name, type, source, added_date) "
            "VALUES (?, ?, ?, ?, ?)",
            (ip, threat_name, type, source, datetime.utcnow().isoformat()),
        )
        self.conn.commit()

    def bulk_add_ips(
        self,
        records: Sequence[Tuple[str, str, str, str]],
    ) -> int:
        """Bulk-insert IPs.  Each tuple: ``(ip, threat_name, type, source)``.

        Returns the number of **new** rows actually inserted.
        """
        if not records:
            return 0
        now = datetime.utcnow().isoformat()
        rows = [(ip, tn, t, src, now) for ip, tn, t, src in records]
        before = self._count("ioc_ips")
        self.conn.executemany(
            "INSERT OR IGNORE INTO ioc_ips (ip, threat_name, type, source, added_date) "
            "VALUES (?, ?, ?, ?, ?)",
            rows,
        )
        self.conn.commit()
        return self._count("ioc_ips") - before

    def lookup_ip(self, ip: str) -> Optional[Dict[str, Any]]:
        row = self.conn.execute(
            "SELECT * FROM ioc_ips WHERE ip = ?", (ip,)
        ).fetchone()
        return dict(row) if row else None

    def get_all_malicious_ips(self) -> Set[str]:
        """Return every stored malicious IP as a set for fast membership tests."""
        rows = self.conn.execute("SELECT ip FROM ioc_ips").fetchall()
        return {row[0] for row in rows}

    # ------------------------------------------------------------------
    # Domains
    # ------------------------------------------------------------------

    def add_domain(
        self,
        domain: str,
        threat_name: str = "",
        type: str = "C2",
        source: str = "",
    ) -> None:
        self.conn.execute(
            "INSERT OR REPLACE INTO ioc_domains (domain, threat_name, type, source, added_date) "
            "VALUES (?, ?, ?, ?, ?)",
            (domain.lower(), threat_name, type, source, datetime.utcnow().isoformat()),
        )
        self.conn.commit()

    def bulk_add_domains(
        self,
        records: Sequence[Tuple[str, str, str, str]],
    ) -> int:
        """Bulk-insert domains.  Each tuple: ``(domain, threat_name, type, source)``.

        Returns the number of **new** rows actually inserted.
        """
        if not records:
            return 0
        now = datetime.utcnow().isoformat()
        rows = [(d.lower(), tn, t, src, now) for d, tn, t, src in records]
        before = self._count("ioc_domains")
        self.conn.executemany(
            "INSERT OR IGNORE INTO ioc_domains (domain, threat_name, type, source, added_date) "
            "VALUES (?, ?, ?, ?, ?)",
            rows,
        )
        self.conn.commit()
        return self._count("ioc_domains") - before

    def lookup_domain(self, domain: str) -> Optional[Dict[str, Any]]:
        row = self.conn.execute(
            "SELECT * FROM ioc_domains WHERE domain = ?", (domain.lower(),)
        ).fetchone()
        return dict(row) if row else None

    def get_all_malicious_domains(self) -> Set[str]:
        """Return every stored malicious domain as a set."""
        rows = self.conn.execute("SELECT domain FROM ioc_domains").fetchall()
        return {row[0] for row in rows}

    # ------------------------------------------------------------------
    # URLs
    # ------------------------------------------------------------------

    def add_url(
        self,
        url: str,
        threat_name: str = "",
        type: str = "malware_distribution",
        source: str = "",
    ) -> None:
        self.conn.execute(
            "INSERT OR REPLACE INTO ioc_urls (url, threat_name, type, source, added_date) "
            "VALUES (?, ?, ?, ?, ?)",
            (url, threat_name, type, source, datetime.utcnow().isoformat()),
        )
        self.conn.commit()

    def bulk_add_urls(
        self,
        records: Sequence[Tuple[str, str, str, str]],
    ) -> int:
        """Bulk-insert URLs.  Each tuple: ``(url, threat_name, type, source)``.

        Returns the number of **new** rows actually inserted.
        """
        if not records:
            return 0
        now = datetime.utcnow().isoformat()
        rows = [(u, tn, t, src, now) for u, tn, t, src in records]
        before = self._count("ioc_urls")
        self.conn.executemany(
            "INSERT OR IGNORE INTO ioc_urls (url, threat_name, type, source, added_date) "
            "VALUES (?, ?, ?, ?, ?)",
            rows,
        )
        self.conn.commit()
        return self._count("ioc_urls") - before

    def lookup_url(self, url: str) -> Optional[Dict[str, Any]]:
        row = self.conn.execute(
            "SELECT * FROM ioc_urls WHERE url = ?", (url,)
        ).fetchone()
        return dict(row) if row else None

    # ------------------------------------------------------------------
    # Aggregate stats
    # ------------------------------------------------------------------

    def get_stats(self) -> Dict[str, Any]:
        return {
            "ips": self._count("ioc_ips"),
            "domains": self._count("ioc_domains"),
            "urls": self._count("ioc_urls"),
        }
