r"""
 __  __                           _
|  \/  | ___ _ __ ___   ___  _ __(_)
| |\/| |/ _ \ '_ ` _ \ / _ \| '__| |
| |  | |  __/ | | | | | (_) | |  | |
|_|  |_|\___|_| |_| |_|\___/|_|  |_|
                  perfectam memoriam
                       memorilabs.ai
"""

from uuid import uuid4

from memori.storage._base import (
    BaseConversation,
    BaseConversationMessage,
    BaseConversationMessages,
    BaseEntity,
    BaseEntityFact,
    BaseKnowledgeGraph,
    BaseProcess,
    BaseProcessAttribute,
    BaseSchema,
    BaseSchemaVersion,
    BaseSession,
    BaseStorageAdapter,
)
from memori.storage._registry import Registry
from memori.storage.migrations._postgresql import migrations


class Conversation(BaseConversation):
    def __init__(self, conn: BaseStorageAdapter):
        super().__init__(conn)
        self.message = ConversationMessage(conn)
        self.messages = ConversationMessages(conn)

    def create(self, session_id, timeout_minutes: int):
        existing = (
            self.conn.execute(
                """
                SELECT c.id,
                       COALESCE(MAX(m.date_created), c.date_created) as last_activity
                  FROM memori_conversation c
                  LEFT JOIN memori_conversation_message m ON m.conversation_id = c.id
                 WHERE c.session_id = %s
                 GROUP BY c.id, c.date_created
                """,
                (session_id,),
            )
            .mappings()
            .fetchone()
        )

        if existing:
            result = self.conn.execute(
                """
                SELECT EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - %s::timestamp)) / 60 as minutes_since_activity
                """,
                (existing["last_activity"],),
            ).fetchone()

            if result[0] <= timeout_minutes:
                return existing["id"]

        uuid = str(uuid4())
        self.conn.execute(
            """
            INSERT INTO memori_conversation(
                uuid,
                session_id
            ) VALUES (
                %s,
                %s
            )
            ON CONFLICT DO NOTHING
            """,
            (uuid, session_id),
        )
        self.conn.commit()

        return (
            self.conn.execute(
                """
                SELECT id
                  FROM memori_conversation
                 WHERE session_id = %s
                """,
                (session_id,),
            )
            .mappings()
            .fetchone()
            .get("id", None)
        )

    def update(self, id: int, summary: str):
        if summary is None:
            return self

        self.conn.execute(
            """
            UPDATE memori_conversation
               SET summary = %s
             WHERE id = %s
            """,
            (
                summary,
                id,
            ),
        )
        self.conn.commit()

        return self

    def read(self, id: int) -> dict | None:
        result = (
            self.conn.execute(
                """
                SELECT id, uuid, session_id, summary, date_created, date_updated
                  FROM memori_conversation
                 WHERE id = %s
                """,
                (id,),
            )
            .mappings()
            .fetchone()
        )

        if result is None:
            return None

        return dict(result)


class ConversationMessage(BaseConversationMessage):
    def create(self, conversation_id: int, role: str, type: str, content: str):
        self.conn.execute(
            """
            INSERT INTO memori_conversation_message(
                uuid,
                conversation_id,
                role,
                type,
                content
            ) VALUES (
                %s,
                %s,
                %s,
                %s,
                %s
            )
            """,
            (
                str(uuid4()),
                conversation_id,
                role,
                type,
                content,
            ),
        )


class ConversationMessages(BaseConversationMessages):
    def read(self, conversation_id: int):
        results = (
            self.conn.execute(
                """
                SELECT role,
                       content
                  FROM memori_conversation_message
                 WHERE conversation_id = %s
                """,
                (conversation_id,),
            )
            .mappings()
            .fetchall()
        )

        messages = []
        for result in results:
            messages.append({"content": result["content"], "role": result["role"]})

        return messages


class Entity(BaseEntity):
    def create(self, external_id: str):
        self.conn.execute(
            """
            INSERT INTO memori_entity(
                uuid,
                external_id
            ) VALUES (
                %s,
                %s
            )
            ON CONFLICT DO NOTHING
            """,
            (str(uuid4()), external_id),
        )
        self.conn.commit()

        return (
            self.conn.execute(
                """
                SELECT id
                  FROM memori_entity
                 WHERE external_id = %s
                """,
                (external_id,),
            )
            .mappings()
            .fetchone()
            .get("id", None)
        )


class EntityFact(BaseEntityFact):
    def create(self, entity_id: int, facts: list, fact_embeddings: list | None = None):
        if facts is None or len(facts) == 0:
            return self

        from memori._utils import generate_uniq
        from memori.llm._embeddings import format_embedding_for_db

        dialect = self.conn.get_dialect()

        for i, fact in enumerate(facts):
            embedding = (
                fact_embeddings[i]
                if fact_embeddings and i < len(fact_embeddings)
                else []
            )
            embedding_formatted = format_embedding_for_db(embedding, dialect)
            uniq = generate_uniq([fact])

            self.conn.execute(
                """
                INSERT INTO memori_entity_fact(
                    uuid,
                    entity_id,
                    content,
                    content_embedding,
                    num_times,
                    date_last_time,
                    uniq
                ) VALUES (
                    %s,
                    %s,
                    %s,
                    %s,
                    1,
                    CURRENT_TIMESTAMP,
                    %s
                )
                ON CONFLICT (entity_id, uniq) DO UPDATE SET
                    num_times = memori_entity_fact.num_times + 1,
                    date_last_time = CURRENT_TIMESTAMP
                """,
                (
                    str(uuid4()),
                    entity_id,
                    fact,
                    embedding_formatted,
                    uniq,
                ),
            )

        return self

    def get_embeddings(self, entity_id: int, limit: int = 1000):
        return (
            self.conn.execute(
                """
                SELECT id,
                       content_embedding
                  FROM memori_entity_fact
                 WHERE entity_id = %s
                 LIMIT %s
                """,
                (entity_id, limit),
            )
            .mappings()
            .fetchall()
        )

    def get_facts_by_ids(self, fact_ids: list[int]):
        return (
            self.conn.execute(
                """
                SELECT id,
                       content
                  FROM memori_entity_fact
                 WHERE id = ANY(%s)
                """,
                (fact_ids,),
            )
            .mappings()
            .fetchall()
        )


class KnowledgeGraph(BaseKnowledgeGraph):
    def create(self, entity_id: int, semantic_triples: list):
        if semantic_triples is None or len(semantic_triples) == 0:
            return self

        from memori._utils import generate_uniq

        for semantic_triple in semantic_triples:
            uniq = generate_uniq(
                [semantic_triple.subject_name, semantic_triple.subject_type]
            )

            self.conn.execute(
                """
                INSERT INTO memori_subject(
                    uuid,
                    name,
                    type,
                    uniq
                ) VALUES (
                    %s,
                    %s,
                    %s,
                    %s
                )
                ON CONFLICT DO NOTHING
                """,
                (
                    str(uuid4()),
                    semantic_triple.subject_name,
                    semantic_triple.subject_type,
                    uniq,
                ),
            )
            self.conn.commit()

            subject_id = (
                self.conn.execute(
                    """
                    SELECT id
                      FROM memori_subject
                     WHERE uniq = %s
                    """,
                    (uniq,),
                )
                .mappings()
                .fetchone()
                .get("id", None)
            )

            uniq = generate_uniq([semantic_triple.predicate])

            self.conn.execute(
                """
                INSERT INTO memori_predicate(
                    uuid,
                    content,
                    uniq
                ) VALUES (
                    %s,
                    %s,
                    %s
                )
                ON CONFLICT DO NOTHING
                """,
                (
                    str(uuid4()),
                    semantic_triple.predicate,
                    uniq,
                ),
            )
            self.conn.commit()

            predicate_id = (
                self.conn.execute(
                    """
                    SELECT id
                      FROM memori_predicate
                     WHERE uniq = %s
                    """,
                    (uniq,),
                )
                .mappings()
                .fetchone()
                .get("id", None)
            )

            uniq = generate_uniq(
                [semantic_triple.object_name, semantic_triple.object_type]
            )

            self.conn.execute(
                """
                INSERT INTO memori_object(
                    uuid,
                    name,
                    type,
                    uniq
                ) VALUES (
                    %s,
                    %s,
                    %s,
                    %s
                )
                ON CONFLICT DO NOTHING
                """,
                (
                    str(uuid4()),
                    semantic_triple.object_name,
                    semantic_triple.object_type,
                    uniq,
                ),
            )
            self.conn.commit()

            object_id = (
                self.conn.execute(
                    """
                    SELECT id
                      FROM memori_object
                     WHERE uniq = %s
                    """,
                    (uniq,),
                )
                .mappings()
                .fetchone()
                .get("id", None)
            )

            if (
                entity_id is not None
                and subject_id is not None
                and predicate_id is not None
                and object_id is not None
            ):
                self.conn.execute(
                    """
                    INSERT INTO memori_knowledge_graph(
                        uuid,
                        entity_id,
                        subject_id,
                        predicate_id,
                        object_id,
                        num_times,
                        date_last_time
                    ) VALUES (
                        %s,
                        %s,
                        %s,
                        %s,
                        %s,
                        1,
                        CURRENT_TIMESTAMP
                    )
                    ON CONFLICT (entity_id, subject_id, predicate_id, object_id) DO UPDATE SET
                        num_times = memori_knowledge_graph.num_times + 1,
                        date_last_time = CURRENT_TIMESTAMP
                    """,
                    (str(uuid4()), entity_id, subject_id, predicate_id, object_id),
                )

        return self


class Process(BaseProcess):
    def create(self, external_id: str):
        self.conn.execute(
            """
            INSERT INTO memori_process(
                uuid,
                external_id
            ) VALUES (
                %s,
                %s
            )
            ON CONFLICT DO NOTHING
            """,
            (str(uuid4()), external_id),
        )
        self.conn.commit()

        return (
            self.conn.execute(
                """
                SELECT id
                  FROM memori_process
                 WHERE external_id = %s
                """,
                (external_id,),
            )
            .mappings()
            .fetchone()
            .get("id", None)
        )


class ProcessAttribute(BaseProcessAttribute):
    def create(self, process_id: int, attributes: list):
        if attributes is None or len(attributes) == 0:
            return self

        from memori._utils import generate_uniq

        for attribute in attributes:
            uniq = generate_uniq([attribute])

            self.conn.execute(
                """
                INSERT INTO memori_process_attribute(
                    uuid,
                    process_id,
                    content,
                    num_times,
                    date_last_time,
                    uniq
                ) VALUES (
                    %s,
                    %s,
                    %s,
                    1,
                    CURRENT_TIMESTAMP,
                    %s
                )
                ON CONFLICT (process_id, uniq) DO UPDATE SET
                    num_times = memori_process_attribute.num_times + 1,
                    date_last_time = CURRENT_TIMESTAMP
                """,
                (
                    str(uuid4()),
                    process_id,
                    attribute,
                    uniq,
                ),
            )

        return self


class Session(BaseSession):
    def create(self, uuid: str, entity_id: int, process_id: int):
        self.conn.execute(
            """
            INSERT INTO memori_session(
                uuid,
                entity_id,
                process_id
            ) VALUES (
                %s,
                %s,
                %s
            )
            ON CONFLICT DO NOTHING
            """,
            (str(uuid), entity_id, process_id),
        )
        self.conn.commit()

        return (
            self.conn.execute(
                """
                SELECT id
                  FROM memori_session
                 WHERE uuid = %s
                """,
                (str(uuid),),
            )
            .mappings()
            .fetchone()
            .get("id", None)
        )


class Schema(BaseSchema):
    def __init__(self, conn: BaseStorageAdapter):
        super().__init__(conn)
        self.version = SchemaVersion(conn)


class SchemaVersion(BaseSchemaVersion):
    def create(self, num: int):
        self.conn.execute(
            """
            INSERT INTO memori_schema_version(
                num
            ) VALUES (
                %s
            )
            """,
            (num,),
        )

    def delete(self):
        self.conn.execute(
            """
            DELETE FROM memori_schema_version
            """
        )

    def read(self):
        return (
            self.conn.execute(
                """
                SELECT num
                  FROM memori_schema_version
                """
            )
            .mappings()
            .fetchone()
            .get("num", None)
        )


@Registry.register_driver("postgresql")
@Registry.register_driver("cockroachdb")
class Driver:
    """PostgreSQL storage driver (also supports CockroachDB).

    Attributes:
        migrations: Database schema migrations for PostgreSQL-compatible databases.
        requires_rollback_on_error: PostgreSQL aborts transactions when a query
            fails and requires an explicit ROLLBACK before executing new queries.
    """

    migrations = migrations
    requires_rollback_on_error = True

    def __init__(self, conn: BaseStorageAdapter):
        self.conversation = Conversation(conn)
        self.entity = Entity(conn)
        self.entity_fact = EntityFact(conn)
        self.knowledge_graph = KnowledgeGraph(conn)
        self.process = Process(conn)
        self.process_attribute = ProcessAttribute(conn)
        self.schema = Schema(conn)
        self.session = Session(conn)
