"""A deployment's storage, assembled by dependency injection from `storage.json` (PORT-0).

The storage file names backends and resources only: which state store, which vector index,
where derived files go and which environment variable holds a connection string. It is separate
from the semantic policy (`configs/universal.json`) and never selects anything by knowledge base,
domain or language. Credentials are never written in it — only the name of the environment
variable that holds them."""

from __future__ import annotations

import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Literal

from pydantic import Field

from ..contracts import Contract
from ..errors import ConfigurationError
from ..retrieval.data import canonical_hash
from .catalog import CatalogCache
from .local_exact import LocalExactIndex
from .release import ReleaseCoordinator


class StateConfig(Contract):
    backend: Literal["sqlite", "postgres"]
    path: str | None = None  # sqlite: file path (relative to the deployment root) or ":memory:"
    dsn_env: str | None = Field(default=None, pattern=r"^[A-Z][A-Z0-9_]*$")  # postgres
    schema_name: str | None = Field(default=None, pattern=r"^[a-z_][a-z0-9_]{0,62}$")


class VectorConfig(Contract):
    index: Literal["local_exact", "pgvector", "qdrant"] = "local_exact"
    # local_exact: "beside" = `<sqlite path>.dense`; a path: that directory; null: resident.
    matrix_dir: str | None = "beside"
    # local_exact physical layout: "rebuild" (E0H, default) or an N1F-2 incremental layout.
    layout: Literal["rebuild", "base_delta", "segments", "paged"] = "rebuild"
    recipe: dict | None = None
    # pgvector / qdrant: where the service is (environment variable names only) and what the
    # ranking guarantees: "native" (exact over the service's float32 vectors) or "canonical"
    # (candidates rescored from the canonical float64 vectors with a completeness proof).
    precision: Literal["native", "canonical"] = "canonical"
    dsn_env: str | None = Field(default=None, pattern=r"^[A-Z][A-Z0-9_]*$")
    url_env: str | None = Field(default=None, pattern=r"^[A-Z][A-Z0-9_]*$")
    api_key_env: str | None = Field(default=None, pattern=r"^[A-Z][A-Z0-9_]*$")
    schema_name: str | None = Field(default=None, pattern=r"^[a-z_][a-z0-9_]{0,62}$")
    prefix: str = Field(default="rvg", pattern=r"^[a-z][a-z0-9_]{0,30}$")
    overlay_limit: int = Field(default=20000, ge=0)


class StorageConfig(Contract):
    schema_version: Literal[1]
    state: StateConfig
    vectors: VectorConfig = VectorConfig()

    def fingerprint(self) -> str:
        return canonical_hash(self.model_dump(mode="json"))


@dataclass
class Storage:
    state: object
    blobs: object
    index: object
    catalogs: CatalogCache
    release: ReleaseCoordinator
    config_hash: str | None = None
    projector: object | None = None
    _described: dict | None = field(default=None, repr=False)

    def describe(self):
        """The deployment as recorded with every result: backends, the index's precision
        class and the storage configuration's hash (no paths, no credentials)."""
        if self._described is None:
            state, index = self.state.describe(), self.index.capabilities()
            body = {
                "state": state["backend"],
                "index": index["index"],
                "precision": index["search"],
                "config_hash": self.config_hash,
            }
            self._described = body | {
                "fingerprint": canonical_hash({"state": state, "index": index, **body})
            }
        return dict(self._described)

    def close(self):
        self.state.close()
        backend = getattr(self.index, "backend", None)
        if backend is not None:
            backend.close()


def local_storage(
    state,
    *,
    matrix_dir=None,
    config_hash=None,
    index=None,
    catalogs=None,
    layout="rebuild",
    recipe=None,
):
    """A state store that is also its vector blob store, searched by the local exact index."""
    if index is None and layout != "rebuild":
        from .incremental import IncrementalExactIndex

        index = IncrementalExactIndex(state, matrix_dir=matrix_dir, layout=layout, recipe=recipe)
    return Storage(
        state=state,
        blobs=state,
        index=index if index is not None else LocalExactIndex(state, matrix_dir=matrix_dir),
        catalogs=catalogs if catalogs is not None else CatalogCache(state),
        release=ReleaseCoordinator(state),
        config_hash=config_hash,
    )


def remote_storage(state, backend, *, precision="canonical", overlay_limit=20000, config_hash=None):
    """A state store that is its vector blob store, projected into a vector service."""
    from .projection import Projector
    from .remote import RemoteIndex

    catalogs = CatalogCache(state)
    projector = Projector(state, state, backend, catalogs)
    index = RemoteIndex(
        state, state, backend, precision=precision, projector=projector, overlay_limit=overlay_limit
    )
    storage = Storage(
        state, state, index, catalogs, ReleaseCoordinator(state), config_hash=config_hash
    )
    storage.projector = projector
    return storage


def as_storage(value):
    if isinstance(value, Storage):
        return value
    storage = getattr(value, "storage", None)
    if isinstance(storage, Storage):
        return storage
    raise TypeError("storage_required")


def load_config(source) -> StorageConfig:
    if isinstance(source, StorageConfig):
        return source
    if isinstance(source, (str, Path)):
        source = json.loads(Path(source).read_text(encoding="utf-8"))
    return StorageConfig.model_validate(source)


def open_storage(source, *, root=None, environ=None):
    """Open the deployment a storage configuration names."""
    config = load_config(source)
    root = Path(root) if root is not None else Path.cwd()
    environ = os.environ if environ is None else environ
    matrix = config.vectors.matrix_dir
    if config.state.backend == "sqlite":
        from .sqlite import SQLiteState

        if not config.state.path:
            raise ConfigurationError("sqlite_path_required")
        path = config.state.path if config.state.path == ":memory:" else root / config.state.path
        state = SQLiteState(path)
        if matrix == "beside":
            matrix = None if path == ":memory:" else Path(f"{path}.dense")
    else:
        if not config.state.dsn_env or config.state.dsn_env not in environ:
            raise ConfigurationError("postgres_dsn_environment_variable_missing")
        if matrix == "beside" and config.vectors.index == "local_exact":
            raise ConfigurationError("matrix_dir_required_for_postgres")
        from .postgres import PostgresState

        state = PostgresState(environ[config.state.dsn_env], schema=config.state.schema_name)
    vectors = config.vectors
    if vectors.index == "pgvector":
        from .pgvector import PgvectorBackend

        if not vectors.dsn_env or vectors.dsn_env not in environ:
            raise ConfigurationError("pgvector_dsn_environment_variable_missing")
        backend = PgvectorBackend(
            environ[vectors.dsn_env], schema=vectors.schema_name or "rvg_index"
        )
        return remote_storage(
            state,
            backend,
            precision=vectors.precision,
            overlay_limit=vectors.overlay_limit,
            config_hash=config.fingerprint(),
        )
    if vectors.index == "qdrant":
        from .qdrant import QdrantBackend

        if not vectors.url_env or vectors.url_env not in environ:
            raise ConfigurationError("qdrant_url_environment_variable_missing")
        key = environ.get(vectors.api_key_env) if vectors.api_key_env else None
        backend = QdrantBackend(environ[vectors.url_env], prefix=vectors.prefix, api_key=key)
        return remote_storage(
            state,
            backend,
            precision=vectors.precision,
            overlay_limit=vectors.overlay_limit,
            config_hash=config.fingerprint(),
        )
    if matrix is not None:
        matrix = Path(matrix) if Path(matrix).is_absolute() else root / matrix
    return local_storage(
        state,
        matrix_dir=matrix,
        config_hash=config.fingerprint(),
        layout=vectors.layout,
        recipe=vectors.recipe,
    )
