"""
BIBIVIDI — Script Library
SQLite-backed storage for BSS (BIBIVIDI Skill Specification) scripts.
Schema aligns with BSS v1: skill.json metadata + skill.py code.
"""
from __future__ import annotations

import hashlib
import json
import logging
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any

import aiosqlite

from bvd.config import settings

logger = logging.getLogger(__name__)

CURRENT_SCHEMA_VERSION = 7

MIGRATIONS: dict[int, str] = {
    # v1 = initial schema — no SQL needed (DDL creates tables for fresh installs)
    2: """
        ALTER TABLE scripts
        ADD COLUMN status TEXT NOT NULL DEFAULT 'active'
            CHECK(status IN ('active', 'pending'));
    """,
    3: """
        ALTER TABLE scripts ADD COLUMN chain_ids   TEXT NOT NULL DEFAULT '[]';
        ALTER TABLE scripts ADD COLUMN chain_mode  TEXT NOT NULL DEFAULT 'sequential';
        ALTER TABLE scripts ADD COLUMN bss_version TEXT NOT NULL DEFAULT '1.0';
        ALTER TABLE scripts ADD COLUMN inputs_type  TEXT;
        ALTER TABLE scripts ADD COLUMN outputs_type TEXT;
        CREATE TABLE IF NOT EXISTS chain_steps (
            id           INTEGER PRIMARY KEY AUTOINCREMENT,
            execution_id INTEGER NOT NULL REFERENCES executions(id),
            step_num     INTEGER NOT NULL,
            skill_id     TEXT NOT NULL,
            tokens_in    INTEGER NOT NULL,
            tokens_out   INTEGER NOT NULL,
            latency_ms   REAL
        );
    """,
    4: """
        ALTER TABLE executions ADD COLUMN request_id TEXT;
        ALTER TABLE executions ADD COLUMN feedback   TEXT;
    """,
    5: """
        ALTER TABLE executions ADD COLUMN model_name               TEXT;
        ALTER TABLE executions ADD COLUMN model_price_per_token_eur REAL;
    """,
    6: """
        ALTER TABLE scripts ADD COLUMN marketplace_price_eur REAL;
        ALTER TABLE scripts ADD COLUMN author_id             TEXT;
        ALTER TABLE scripts ADD COLUMN step_weights          TEXT;

        CREATE TABLE IF NOT EXISTS marketplace_payouts (
            id                INTEGER PRIMARY KEY AUTOINCREMENT,
            execution_id      INTEGER NOT NULL REFERENCES executions(id),
            skill_id          TEXT NOT NULL,
            author_id         TEXT,
            role              TEXT NOT NULL CHECK(role IN ('chain', 'dep')),
            step_num          INTEGER,
            weight            REAL NOT NULL,
            gross_revenue_eur REAL NOT NULL,
            author_share_eur  REAL NOT NULL,
            status            TEXT NOT NULL DEFAULT 'pending'
                                  CHECK(status IN ('pending', 'paid',
                                                   'below_threshold', 'failed')),
            created_at        TEXT NOT NULL,
            paid_at           TEXT
        );
    """,
    7: """
        ALTER TABLE executions ADD COLUMN license_key_hash TEXT;
    """,
}

_SCHEMA_VERSION_DDL = """
CREATE TABLE IF NOT EXISTS schema_version (
    version INTEGER NOT NULL
);
"""

DDL = """
CREATE TABLE IF NOT EXISTS scripts (
    id          TEXT PRIMARY KEY,          -- BSS skill id (slug)
    name        TEXT NOT NULL,
    description TEXT NOT NULL,
    version     TEXT NOT NULL DEFAULT '1.0.0',
    author      TEXT,
    tags        TEXT,                      -- JSON array
    code        TEXT NOT NULL,             -- skill.py content (or '' for pure chain)
    embedding   BLOB,                      -- numpy float32 array (serialised)
    created_at  TEXT NOT NULL,
    updated_at  TEXT NOT NULL,
    usage_count INTEGER NOT NULL DEFAULT 0,
    avg_saving  REAL NOT NULL DEFAULT 0.0, -- avg token saving ratio
    status      TEXT NOT NULL DEFAULT 'active'
                    CHECK(status IN ('active', 'pending')),
                                           -- 'pending' = matched but not executed until approved
    chain_ids   TEXT NOT NULL DEFAULT '[]',  -- BSS 1.1: JSON array of dep skill IDs
    chain_mode  TEXT NOT NULL DEFAULT 'sequential',
    bss_version TEXT NOT NULL DEFAULT '1.0',
    inputs_type          TEXT,
    outputs_type         TEXT,
    marketplace_price_eur REAL,    -- EUR per execution; NULL = free
    author_id            TEXT,     -- Stripe Connect account ID for payouts
    step_weights         TEXT      -- JSON dict: {skill_id: weight} for dep revenue split
);

CREATE TABLE IF NOT EXISTS marketplace_payouts (
    id                INTEGER PRIMARY KEY AUTOINCREMENT,
    execution_id      INTEGER NOT NULL REFERENCES executions(id),
    skill_id          TEXT NOT NULL,      -- which skill earns this share
    author_id         TEXT,               -- Stripe Connect account ID (NULL = platform keeps)
    role              TEXT NOT NULL CHECK(role IN ('chain', 'dep')),
    step_num          INTEGER,            -- NULL for chain author; 0-based index for deps
    weight            REAL NOT NULL,      -- fraction of gross_revenue going to this author
    gross_revenue_eur REAL NOT NULL,      -- execution-level total revenue
    author_share_eur  REAL NOT NULL,      -- gross_revenue_eur × weight
    status            TEXT NOT NULL DEFAULT 'pending'
                          CHECK(status IN ('pending', 'paid', 'below_threshold', 'failed')),
    created_at        TEXT NOT NULL,
    paid_at           TEXT
);

CREATE TABLE IF NOT EXISTS executions (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    script_id       TEXT NOT NULL REFERENCES scripts(id),
    tokens_before   INTEGER NOT NULL,
    tokens_after    INTEGER NOT NULL,
    latency_ms      REAL,
    executed_at     TEXT NOT NULL,
    request_id               TEXT,   -- X-BIBIVIDI-Request-ID value; used for user feedback
    feedback                 TEXT,   -- 'bad' | NULL
    model_name               TEXT,   -- model field from request body (audit trail)
    model_price_per_token_eur REAL,  -- EUR/token at execution time (immutable audit trail)
    license_key_hash         TEXT    -- SHA-256 of license key; NULL = free/local tier
);

CREATE TABLE IF NOT EXISTS chain_steps (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    execution_id INTEGER NOT NULL REFERENCES executions(id),
    step_num     INTEGER NOT NULL,
    skill_id     TEXT NOT NULL,
    tokens_in    INTEGER NOT NULL,
    tokens_out   INTEGER NOT NULL,
    latency_ms   REAL
);
"""


@dataclass
class Script:
    id: str
    name: str
    description: str
    code: str
    version: str = "1.0.0"
    author: str = ""
    tags: list[str] = field(default_factory=list)
    embedding: bytes | None = None
    created_at: str = field(default_factory=lambda: _now())
    updated_at: str = field(default_factory=lambda: _now())
    usage_count: int = 0
    avg_saving: float = 0.0
    status: str = "active"    # 'active' | 'pending'
    # BSS 1.1 chaining fields
    chain_ids: list[str] = field(default_factory=list)   # ordered dep skill IDs
    chain_mode: str = "sequential"
    bss_version: str = "1.0"
    inputs_type: str | None = None
    outputs_type: str | None = None
    # Marketplace fields
    marketplace_price_eur: float | None = None   # EUR per execution; None = free
    author_id: str | None = None                 # Stripe Connect account ID
    step_weights: dict[str, float] | None = None # custom dep revenue weights

    @property
    def is_chain(self) -> bool:
        """True when this skill is a BSS 1.1 chain (has dep step IDs)."""
        return bool(self.chain_ids)

    @property
    def is_paid(self) -> bool:
        """True when this skill has a marketplace price."""
        return self.marketplace_price_eur is not None and self.marketplace_price_eur > 0


def _now() -> str:
    return datetime.now(UTC).isoformat()


class Library:
    def __init__(self) -> None:
        self._db: aiosqlite.Connection | None = None

    @property
    def db(self) -> aiosqlite.Connection:
        assert self._db is not None, "Library.init() has not been called"
        return self._db

    async def init(self) -> None:
        assert settings.db_path is not None
        self._db = await aiosqlite.connect(settings.db_path)
        self.db.row_factory = aiosqlite.Row
        await self._run_migrations()
        await self.db.executescript(DDL)
        await self.db.commit()
        count = await self.count()
        logger.info("Library initialised: %d scripts in %s", count, settings.db_path)

    async def _run_migrations(self) -> None:
        # Ensure version tracking table exists
        await self.db.executescript(_SCHEMA_VERSION_DDL)
        await self.db.commit()

        # Determine current version
        async with self.db.execute("SELECT version FROM schema_version") as cur:
            row = await cur.fetchone()

        if row is None:
            # No version row yet — detect whether this is a fresh or existing install
            async with self.db.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='scripts'"
            ) as cur:
                has_scripts = await cur.fetchone() is not None

            current_version = 1 if has_scripts else 0
        else:
            current_version = row[0]

        if current_version == CURRENT_SCHEMA_VERSION:
            return  # already up to date

        if current_version == 0:
            # Fresh install: DDL (run after this method) creates tables with the full schema.
            # No ALTER statements needed — just stamp the current version.
            await self.db.execute(
                "INSERT OR REPLACE INTO schema_version(rowid, version) VALUES (1, ?)",
                (CURRENT_SCHEMA_VERSION,),
            )
            await self.db.commit()
            return

        logger.info(
            "Migrating schema: v%d → v%d", current_version, CURRENT_SCHEMA_VERSION
        )

        for v in range(current_version + 1, CURRENT_SCHEMA_VERSION + 1):
            sql = MIGRATIONS.get(v, "")
            if sql.strip():
                await self.db.executescript(sql)
            await self.db.execute(
                "INSERT OR REPLACE INTO schema_version(rowid, version) VALUES (1, ?)", (v,)
            )
            await self.db.commit()
            logger.info("Migration to v%d applied", v)

    async def close(self) -> None:
        if self._db:
            await self._db.close()

    # ── CRUD ──────────────────────────────────────────────────

    async def save(self, script: Script) -> None:
        """Insert or replace a script."""
        await self.db.execute(
            """
            INSERT INTO scripts
                (id, name, description, version, author, tags, code,
                 embedding, created_at, updated_at, usage_count, avg_saving, status,
                 chain_ids, chain_mode, bss_version, inputs_type, outputs_type,
                 marketplace_price_eur, author_id, step_weights)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON CONFLICT(id) DO UPDATE SET
                name=excluded.name,
                description=excluded.description,
                version=excluded.version,
                code=excluded.code,
                embedding=excluded.embedding,
                updated_at=excluded.updated_at,
                tags=excluded.tags,
                chain_ids=excluded.chain_ids,
                chain_mode=excluded.chain_mode,
                bss_version=excluded.bss_version,
                inputs_type=excluded.inputs_type,
                outputs_type=excluded.outputs_type,
                marketplace_price_eur=excluded.marketplace_price_eur,
                author_id=excluded.author_id,
                step_weights=excluded.step_weights
            """,
            (
                script.id,
                script.name,
                script.description,
                script.version,
                script.author,
                json.dumps(script.tags),
                script.code,
                script.embedding,
                script.created_at,
                script.updated_at,
                script.usage_count,
                script.avg_saving,
                script.status,
                json.dumps(script.chain_ids),
                script.chain_mode,
                script.bss_version,
                script.inputs_type,
                script.outputs_type,
                script.marketplace_price_eur,
                script.author_id,
                json.dumps(script.step_weights) if script.step_weights else None,
            ),
        )
        await self.db.commit()

    async def get(self, script_id: str) -> Script | None:
        async with self.db.execute(
            "SELECT * FROM scripts WHERE id = ?", (script_id,)
        ) as cur:
            row = await cur.fetchone()
        return _row_to_script(row) if row else None

    async def all(self) -> list[Script]:
        async with self.db.execute("SELECT * FROM scripts ORDER BY usage_count DESC") as cur:
            rows = await cur.fetchall()
        return [_row_to_script(r) for r in rows]

    async def all_with_embeddings(self) -> list[Script]:
        """Return only scripts that have an embedding (used for matching)."""
        async with self.db.execute(
            "SELECT * FROM scripts WHERE embedding IS NOT NULL"
        ) as cur:
            rows = await cur.fetchall()
        return [_row_to_script(r) for r in rows]

    async def increment_usage(self, script_id: str, saving_ratio: float) -> None:
        await self.db.execute(
            """
            UPDATE scripts
            SET usage_count = usage_count + 1,
                avg_saving  = (avg_saving * usage_count + ?) / (usage_count + 1),
                updated_at  = ?
            WHERE id = ?
            """,
            (saving_ratio, _now(), script_id),
        )
        await self.db.commit()

    async def approve(self, script_id: str) -> bool:
        """Set a pending skill to active. Returns True if the skill was found."""
        async with self.db.execute(
            "SELECT id FROM scripts WHERE id = ? AND status = 'pending'", (script_id,)
        ) as cur:
            row = await cur.fetchone()
        if not row:
            return False
        await self.db.execute(
            "UPDATE scripts SET status = 'active', updated_at = ? WHERE id = ?",
            (_now(), script_id),
        )
        await self.db.commit()
        return True

    async def list_pending(self) -> list[Script]:
        """Return all skills awaiting approval."""
        async with self.db.execute(
            "SELECT * FROM scripts WHERE status = 'pending' ORDER BY created_at DESC"
        ) as cur:
            rows = await cur.fetchall()
        return [_row_to_script(r) for r in rows]

    async def get_many(self, script_ids: list[str]) -> list[Script | None]:
        """Fetch multiple scripts by ID in order. Missing IDs return None."""
        return [await self.get(sid) for sid in script_ids]

    @staticmethod
    def build_type_graph(
        scripts: list[Script],
    ) -> dict[str, list[tuple[str, Script]]]:
        """Build a directed type-compatibility graph from typed skills.

        Returns ``{input_type: [(output_type, script), ...]}``.
        Only active skills with both ``inputs_type`` and ``outputs_type``
        declared are included.  Skills with equal input and output types are
        excluded (identity transforms are not useful in a pipeline).
        """
        graph: dict[str, list[tuple[str, Script]]] = {}
        for s in scripts:
            if (
                s.inputs_type
                and s.outputs_type
                and s.status == "active"
                and s.inputs_type != s.outputs_type
            ):
                graph.setdefault(s.inputs_type, []).append((s.outputs_type, s))
        return graph

    async def _check_no_cycle(self, new_id: str, chain_ids: list[str]) -> None:
        """DFS from each chain_id; raise ValueError if new_id is reachable (cycle)."""
        visited: set[str] = set()
        queue = list(chain_ids)
        while queue:
            current = queue.pop()
            if current == new_id:
                raise ValueError(
                    f"Circular chain detected: '{new_id}' references itself transitively."
                )
            if current in visited:
                continue
            visited.add(current)
            dep = await self.get(current)
            if dep and dep.chain_ids:
                queue.extend(dep.chain_ids)

    async def delete(self, script_id: str) -> None:
        await self.db.execute("DELETE FROM scripts WHERE id = ?", (script_id,))
        await self.db.commit()

    async def count(self) -> int:
        async with self.db.execute("SELECT COUNT(*) FROM scripts") as cur:
            row = await cur.fetchone()
        return row[0] if row else 0

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

    def to_bss(self, script: Script) -> dict[str, Any]:
        """Export a script as a BSS-compliant skill.json dict (includes SHA-256 hash)."""
        return {
            "bss_version": "1.0",
            "id": script.id,
            "name": script.name,
            "description": script.description,
            "version": script.version,
            "author": script.author,
            "tags": script.tags,
            "entry_point": "skill.py",
            "hash": hashlib.sha256(script.code.encode()).hexdigest(),
            "created_at": script.created_at,
            "updated_at": script.updated_at,
        }

    async def import_bss(
        self,
        skill_json: dict,
        skill_py: str,
        status: str = "active",
        skill_sig: bytes | None = None,
    ) -> Script:
        """Import a BSS skill pack (skill.json + skill.py) into the library.

        Verification layers (both are optional but cumulative when present):

        1. **SHA-256 hash** — if ``skill.json`` has a ``hash`` field, it is
           compared against SHA-256(skill.py). Raises ``ValueError`` on mismatch.

        2. **Ed25519 signature** — if ``skill.json`` has a ``pubkey`` field
           (format ``"ed25519:<base64-encoded-32-bytes>"``) *and* ``skill_sig``
           is provided, the signature is verified against ``skill.py`` bytes.
           Raises ``ValueError`` on failure. If either is absent the check is
           skipped (backwards-compatible with packs that don't yet have a sig).

        ``status`` defaults to 'active' for manually installed packs and
        should be 'pending' for auto-generated skills (requires approval
        before execution).
        """
        actual_hash = hashlib.sha256(skill_py.encode()).hexdigest()
        declared_hash = skill_json.get("hash", "")
        if declared_hash and actual_hash != declared_hash:
            raise ValueError(
                f"BSS hash mismatch for '{skill_json.get('id')}':\n"
                f"  declared : {declared_hash}\n"
                f"  actual   : {actual_hash}\n"
                "skill.py may have been tampered with."
            )

        pubkey_field = skill_json.get("pubkey", "")
        if pubkey_field and skill_sig is not None:
            _verify_ed25519(pubkey_field, skill_sig, skill_py.encode(), skill_json.get("id"))

        chain_ids: list[str] = skill_json.get("chain", [])
        skill_id = skill_json["id"]

        if chain_ids:
            await self._check_no_cycle(skill_id, chain_ids)

        raw_weights = skill_json.get("step_weights")
        step_weights: dict[str, float] | None = None
        if isinstance(raw_weights, dict) and raw_weights:
            step_weights = {str(k): float(v) for k, v in raw_weights.items()}

        script = Script(
            id=skill_id,
            name=skill_json["name"],
            description=skill_json["description"],
            version=skill_json.get("version", "1.0.0"),
            author=skill_json.get("author", ""),
            tags=skill_json.get("tags", []),
            code=skill_py,
            status=status,
            chain_ids=chain_ids,
            chain_mode=skill_json.get("chain_mode", "sequential"),
            bss_version=skill_json.get("bss_version", "1.1" if chain_ids else "1.0"),
            inputs_type=skill_json.get("inputs_type"),
            outputs_type=skill_json.get("outputs_type"),
            marketplace_price_eur=skill_json.get("marketplace_price_eur"),
            author_id=skill_json.get("author_id"),
            step_weights=step_weights,
        )
        await self.save(script)
        return script


def _verify_ed25519(
    pubkey_field: str, signature: bytes, message: bytes, skill_id: str | None
) -> None:
    """Verify an Ed25519 signature for a BSS skill pack.

    ``pubkey_field`` must be in the format ``"ed25519:<base64-encoded-public-key>"``.
    ``signature`` is the raw 64-byte Ed25519 signature.
    Raises ``ValueError`` on invalid format or failed verification.
    """
    import base64

    from cryptography.exceptions import InvalidSignature
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

    prefix = "ed25519:"
    if not pubkey_field.startswith(prefix):
        raise ValueError(
            f"BSS pubkey for '{skill_id}' has unknown format "
            f"(expected 'ed25519:<base64>'): {pubkey_field!r}"
        )
    try:
        pubkey_bytes = base64.b64decode(pubkey_field[len(prefix):])
        pubkey = Ed25519PublicKey.from_public_bytes(pubkey_bytes)
    except Exception as exc:
        raise ValueError(
            f"BSS pubkey for '{skill_id}' could not be loaded: {exc}"
        ) from exc

    try:
        pubkey.verify(signature, message)
    except InvalidSignature:
        raise ValueError(
            f"BSS Ed25519 signature verification failed for '{skill_id}'.\n"
            "skill.py may have been tampered with or signed by a different key."
        )

    logger.debug("Ed25519 signature verified for skill '%s'", skill_id)


def _row_to_script(row: aiosqlite.Row) -> Script:
    raw_weights = row["step_weights"]
    step_weights: dict[str, float] | None = None
    if raw_weights:
        try:
            step_weights = json.loads(raw_weights)
        except (ValueError, TypeError):
            pass
    return Script(
        id=row["id"],
        name=row["name"],
        description=row["description"],
        code=row["code"],
        version=row["version"],
        author=row["author"] or "",
        tags=json.loads(row["tags"] or "[]"),
        embedding=row["embedding"],
        created_at=row["created_at"],
        updated_at=row["updated_at"],
        usage_count=row["usage_count"],
        avg_saving=row["avg_saving"],
        status=row["status"],
        chain_ids=json.loads(row["chain_ids"] or "[]"),
        chain_mode=row["chain_mode"] or "sequential",
        bss_version=row["bss_version"] or "1.0",
        inputs_type=row["inputs_type"],
        outputs_type=row["outputs_type"],
        marketplace_price_eur=row["marketplace_price_eur"],
        author_id=row["author_id"],
        step_weights=step_weights,
    )


# Singleton
library = Library()
