"""Entity node operations for the SurrealDB driver.

Implements Graphiti's ``EntityNodeOperations`` contract against SurrealDB's
``entity`` table. Dynamic node attributes merge into the FLEXIBLE
``attributes`` field rather than leaking into typed columns, preserving
Graphiti's open-world property model.
"""

from __future__ import annotations

import logging

from sibyl_core.graph.surreal.compat.models import EntityNode, NodeNotFoundError
from sibyl_core.graph.surreal.compat.ops._common import (
    QueryExecutor,
    SurrealRecord,
    Transaction,
    build_node_bulk_upsert_query,
    build_node_upsert_query,
    normalize_embedding,
    normalize_records,
    parse_db_date,
    run_query,
)

logger = logging.getLogger(__name__)

_ENTITY_SAVE = build_node_upsert_query(
    "entity",
    (
        "uuid",
        "name",
        "entity_type",
        "summary",
        "description",
        "content",
        "labels",
        "attributes",
        "group_id",
        "created_at",
        "updated_at",
        "project_id",
        "epic_id",
        "task_id",
        "status",
        "priority",
        "complexity",
        "feature",
        "tags",
        "name_embedding",
    ),
)
_ENTITY_SAVE_BULK = build_node_bulk_upsert_query(
    "entity",
    (
        "uuid",
        "name",
        "entity_type",
        "summary",
        "description",
        "content",
        "labels",
        "attributes",
        "group_id",
        "created_at",
        "updated_at",
        "project_id",
        "epic_id",
        "task_id",
        "status",
        "priority",
        "complexity",
        "feature",
        "tags",
        "name_embedding",
    ),
)


def _string_or_none(value: object) -> str | None:
    if value is None or value == "":
        return None
    return str(value)


def _string_list_or_none(value: object) -> list[str] | None:
    if not isinstance(value, list | tuple | set):
        return None
    return [str(item) for item in value if item is not None]


def entity_node_from_record(record: SurrealRecord) -> EntityNode:
    raw_attributes = record["attributes"]
    attributes = (
        {str(key): value for key, value in raw_attributes.items()}
        if isinstance(raw_attributes, dict)
        else {}
    )
    for key in (
        "uuid",
        "name",
        "group_id",
        "name_embedding",
        "summary",
        "created_at",
        "labels",
    ):
        attributes.pop(key, None)

    raw_labels = record.get("labels", [])
    labels = [str(label) for label in raw_labels] if isinstance(raw_labels, list) else []
    group_id = record.get("group_id")
    if isinstance(group_id, str):
        dynamic_label = "Entity_" + group_id.replace("-", "")
        if dynamic_label in labels:
            labels.remove(dynamic_label)

    return EntityNode.model_validate(
        {
            "uuid": record["uuid"],
            "name": record["name"],
            "name_embedding": record.get("name_embedding"),
            "group_id": group_id,
            "labels": labels,
            "created_at": parse_db_date(record["created_at"]),
            "summary": record["summary"],
            "attributes": attributes,
        }
    )


def _entity_save_payload(node: EntityNode) -> SurrealRecord:
    attributes = dict(node.attributes or {})
    return {
        "uuid": node.uuid,
        "name": node.name,
        "entity_type": (node.labels[0] if node.labels else "Entity"),
        "summary": node.summary,
        "description": _string_or_none(attributes.get("description")),
        "content": _string_or_none(attributes.get("content")),
        "labels": list(set([*node.labels, "Entity"])),
        "attributes": attributes,
        "group_id": node.group_id,
        "created_at": node.created_at,
        "updated_at": _string_or_none(attributes.get("updated_at")),
        "project_id": _string_or_none(attributes.get("project_id")),
        "epic_id": _string_or_none(attributes.get("epic_id")),
        "task_id": _string_or_none(attributes.get("task_id")),
        "status": _string_or_none(attributes.get("status")),
        "priority": _string_or_none(attributes.get("priority")),
        "complexity": _string_or_none(attributes.get("complexity")),
        "feature": _string_or_none(attributes.get("feature")),
        "tags": _string_list_or_none(attributes.get("tags")),
        "name_embedding": node.name_embedding,
    }


class SurrealEntityNodeOperations:
    """SurrealDB implementation of Graphiti's EntityNodeOperations."""

    async def save(
        self,
        executor: QueryExecutor,
        node: EntityNode,
        tx: Transaction | None = None,
    ) -> None:
        payload = _entity_save_payload(node)
        await run_query(
            executor,
            tx,
            _ENTITY_SAVE,
            **payload,
        )
        logger.debug("Saved entity to SurrealDB: %s", node.uuid)

    async def save_bulk(
        self,
        executor: QueryExecutor,
        nodes: list[EntityNode],
        tx: Transaction | None = None,
        batch_size: int = 100,
    ) -> None:
        if not nodes:
            return
        for start in range(0, len(nodes), batch_size):
            batch = nodes[start : start + batch_size]
            rows = [_entity_save_payload(n) for n in batch]
            await run_query(
                executor,
                tx,
                _ENTITY_SAVE_BULK,
                rows=rows,
            )

    async def delete(
        self,
        executor: QueryExecutor,
        node: EntityNode,
        tx: Transaction | None = None,
    ) -> None:
        """Delete an entity and cascade any connected relation rows.

        SurrealDB RELATION tables cascade when their endpoints are deleted,
        so no explicit edge cleanup is required here.
        """
        await run_query(
            executor,
            tx,
            "DELETE FROM entity WHERE uuid = $uuid;",
            uuid=node.uuid,
        )
        logger.debug("Deleted entity from SurrealDB: %s", node.uuid)

    async def delete_by_group_id(
        self,
        executor: QueryExecutor,
        group_id: str,
        tx: Transaction | None = None,
        batch_size: int = 100,
    ) -> None:
        del batch_size  # SurrealDB deletes atomically; batch size is advisory
        await run_query(
            executor,
            tx,
            "DELETE FROM entity WHERE group_id = $group_id;",
            group_id=group_id,
        )

    async def delete_by_uuids(
        self,
        executor: QueryExecutor,
        uuids: list[str],
        tx: Transaction | None = None,
        batch_size: int = 100,
    ) -> None:
        del batch_size
        if not uuids:
            return
        await run_query(
            executor,
            tx,
            "DELETE FROM entity WHERE uuid IN $uuids;",
            uuids=uuids,
        )

    async def get_by_uuid(
        self,
        executor: QueryExecutor,
        uuid: str,
    ) -> EntityNode:
        records = normalize_records(
            await executor.execute_query(
                "SELECT * FROM entity WHERE uuid = $uuid LIMIT 1;",
                uuid=uuid,
            )
        )
        if not records:
            raise NodeNotFoundError(uuid)
        return entity_node_from_record(records[0])

    async def get_by_uuids(
        self,
        executor: QueryExecutor,
        uuids: list[str],
    ) -> list[EntityNode]:
        if not uuids:
            return []
        records = normalize_records(
            await executor.execute_query(
                "SELECT * FROM entity WHERE uuid IN $uuids;",
                uuids=uuids,
            )
        )
        return [entity_node_from_record(r) for r in records]

    async def get_by_group_ids(
        self,
        executor: QueryExecutor,
        group_ids: list[str],
        limit: int | None = None,
        uuid_cursor: str | None = None,
    ) -> list[EntityNode]:
        cursor_clause = "AND uuid < $cursor" if uuid_cursor else ""
        limit_clause = f"LIMIT {int(limit)}" if limit is not None else ""
        query = (
            "SELECT * FROM entity "
            "WHERE group_id IN $group_ids "
            f"{cursor_clause} "
            "ORDER BY uuid DESC "
            f"{limit_clause};"
        )
        records = normalize_records(
            await executor.execute_query(
                query,
                group_ids=group_ids,
                cursor=uuid_cursor,
            )
        )
        return [entity_node_from_record(r) for r in records]

    async def load_embeddings(
        self,
        executor: QueryExecutor,
        node: EntityNode,
    ) -> None:
        records = normalize_records(
            await executor.execute_query(
                "SELECT name_embedding FROM entity WHERE uuid = $uuid LIMIT 1;",
                uuid=node.uuid,
            )
        )
        if not records:
            raise NodeNotFoundError(node.uuid)
        node.name_embedding = normalize_embedding(records[0].get("name_embedding"))

    async def load_embeddings_bulk(
        self,
        executor: QueryExecutor,
        nodes: list[EntityNode],
        batch_size: int = 100,
    ) -> None:
        del batch_size
        if not nodes:
            return
        uuids = [n.uuid for n in nodes]
        records = normalize_records(
            await executor.execute_query(
                "SELECT uuid, name_embedding FROM entity WHERE uuid IN $uuids;",
                uuids=uuids,
            )
        )
        embedding_map = {
            str(record["uuid"]): embedding
            for record in records
            if (embedding := normalize_embedding(record.get("name_embedding"))) is not None
        }
        for node in nodes:
            if node.uuid in embedding_map:
                node.name_embedding = embedding_map[node.uuid]


__all__ = ["SurrealEntityNodeOperations"]
