"""Canonical warehouse — durable JSONL storage for domain entities.

Writes canonical records to JSONL files partitioned by entity and
date.  Each line is a self-contained JSON object.

Layout::

    <warehouse_root>/<entity-name>/<YYYY-MM-DD>.jsonl

SQLite hydration loads these partitions into per-entity tables for
querying.
"""

import json
import sqlite3
from dataclasses import asdict, fields
from pathlib import Path
from typing import Iterator, Type

from byteb4rb1e.homeostat.warehouse.schema import (
    ALL_ENTITIES,
    NATURAL_KEYS,
)


class CanonicalWarehouse:
    """Reads and writes canonical domain records to JSONL
    partitions."""

    def __init__(self, root: Path) -> None:
        self._root = root

    def _partition_path(
        self, entity_name: str, date: str
    ) -> Path:
        return self._root / entity_name / f"{date}.jsonl"

    # -- Write ---------------------------------------------------------------

    def append(
        self, entity_name: str, record: object, date: str
    ) -> Path:
        """Append a canonical record to the JSONL partition for
        the given entity and date.

        Returns the path of the partition file written to.
        """
        path = self._partition_path(entity_name, date)
        path.parent.mkdir(parents=True, exist_ok=True)

        line = json.dumps(
            asdict(record), ensure_ascii=False  # type: ignore[arg-type]
        )

        with open(path, "a", encoding="utf-8") as f:
            f.write(line + "\n")

        return path

    def append_many(
        self,
        entity_name: str,
        records: list[object],
        date: str,
    ) -> Path | None:
        """Append multiple canonical records for the same entity
        and date.

        Returns the partition path, or None if records is empty.
        """
        if not records:
            return None

        path = self._partition_path(entity_name, date)
        path.parent.mkdir(parents=True, exist_ok=True)

        with open(path, "a", encoding="utf-8") as f:
            for record in records:
                line = json.dumps(
                    asdict(record),  # type: ignore[arg-type]
                    ensure_ascii=False,
                )
                f.write(line + "\n")

        return path

    # -- Read ----------------------------------------------------------------

    def read_partition(
        self, entity_name: str, date: str
    ) -> list[dict]:
        """Read all records from a single partition as dicts."""
        path = self._partition_path(entity_name, date)
        if not path.exists():
            return []

        records = []
        for line in path.read_text(
            encoding="utf-8"
        ).splitlines():
            if not line.strip():
                continue
            records.append(json.loads(line))
        return records

    def iter_partitions(
        self, entity_name: str | None = None
    ) -> Iterator[tuple[str, str, Path]]:
        """Yield ``(entity_name, date, path)`` for each partition.

        If *entity_name* is given, only yield partitions for that
        entity.
        """
        if entity_name:
            entity_dir = self._root / entity_name
            if entity_dir.is_dir():
                for p in sorted(entity_dir.glob("*.jsonl")):
                    yield entity_name, p.stem, p
            return

        for entity_dir in sorted(self._root.iterdir()):
            if not entity_dir.is_dir():
                continue
            name = entity_dir.name
            if name == "raw":
                continue
            for p in sorted(entity_dir.glob("*.jsonl")):
                yield name, p.stem, p

    # -- SQLite hydration ----------------------------------------------------

    def hydrate(self, db: sqlite3.Connection) -> int:
        """Load all canonical partitions into per-entity SQLite
        tables.

        Creates tables if they do not exist.  Existing rows are
        cleared and reloaded (full rebuild).

        Returns the total number of rows inserted.
        """
        total = 0

        for entity_name, entity_cls in ALL_ENTITIES.items():
            table = entity_name.replace("-", "_")
            col_defs = _column_defs(entity_cls)

            db.execute(
                f"CREATE TABLE IF NOT EXISTS {table}"
                f" ({col_defs})"
            )
            db.execute(f"DELETE FROM {table}")

            for _, date, path in self.iter_partitions(
                entity_name=entity_name
            ):
                for line in path.read_text(
                    encoding="utf-8"
                ).splitlines():
                    if not line.strip():
                        continue
                    row = json.loads(line)
                    cols = list(row.keys())
                    placeholders = ", ".join(
                        "?" for _ in cols
                    )
                    col_names = ", ".join(cols)
                    db.execute(
                        f"INSERT INTO {table}"
                        f" ({col_names})"
                        f" VALUES ({placeholders})",
                        [row[c] for c in cols],
                    )
                    total += 1

            key_cols = NATURAL_KEYS.get(entity_name)
            if key_cols:
                key_str = ", ".join(key_cols)
                before = db.execute(
                    f"SELECT COUNT(*) FROM {table}"
                ).fetchone()[0]
                db.execute(
                    f"DELETE FROM {table}"
                    f" WHERE rowid NOT IN ("
                    f"  SELECT MAX(rowid)"
                    f"  FROM {table}"
                    f"  GROUP BY {key_str}"
                    f")"
                )
                after = db.execute(
                    f"SELECT COUNT(*) FROM {table}"
                ).fetchone()[0]
                total -= before - after

        db.commit()
        return total


# -- Helpers -----------------------------------------------------------------


_TYPE_MAP = {
    int: "INTEGER",
    float: "REAL",
    str: "TEXT",
    bool: "INTEGER",
}

_TYPE_NAME_MAP = {
    "int": "INTEGER",
    "float": "REAL",
    "str": "TEXT",
    "bool": "INTEGER",
}


def _column_defs(cls: Type) -> str:  # type: ignore[type-arg]
    parts = []
    for f in fields(cls):
        if isinstance(f.type, str):
            sql_type = _TYPE_NAME_MAP.get(f.type, "TEXT")
        else:
            sql_type = _TYPE_MAP.get(f.type, "TEXT")  # type: ignore[arg-type]
        parts.append(f"{f.name} {sql_type}")
    return ", ".join(parts)
