"""
Psycopg 3 adapters and row factories for PostGit tables (see schema.sql).

- ``pygit2.Oid`` binds to ``bytea`` (``Oid.raw``) and, via row factories, is
  reconstructed from ``oid`` / ``target_oid`` columns (not from arbitrary
  ``bytea``, e.g. ``object_data``).
- ``git_object_type`` maps to ``GitObjectType`` (``IntEnum`` aligned with
  libgit2 / ``pygit2.GIT_OBJECT_*``).
"""

from __future__ import annotations

from enum import IntEnum
from typing import Any, Sequence

import pygit2
from psycopg import _oids
from psycopg._cursor_base import BaseCursor
from psycopg.abc import AdaptContext
from psycopg.adapt import Buffer, Dumper
from psycopg.connection import Connection
from psycopg.connection_async import AsyncConnection
from psycopg.pq import Format
from psycopg.rows import RowMaker, dict_row, tuple_row
from psycopg.types.enum import EnumInfo, register_enum
from psycopg.types.string import BytesDumper

# libgit2 GIT_REFERENCE_DIRECT / GIT_REFERENCE_SYMBOLIC
_GIT_REFERENCE_OID = 1
_GIT_REFERENCE_SYMBOLIC = 2

_OID_COLUMN_NAMES = frozenset({"oid", "target_oid"})


class GitObjectType(IntEnum):
    """Matches ``git_object_type`` in PostgreSQL and ``pygit2.GIT_OBJECT_*``."""

    COMMIT = 1
    TREE = 2
    BLOB = 3
    TAG = 4


_GIT_OBJECT_TYPE_PG_MAP = {
    GitObjectType.COMMIT: "commit",
    GitObjectType.TREE: "tree",
    GitObjectType.BLOB: "blob",
    GitObjectType.TAG: "tag",
}


class OidDumper(Dumper):
    """Encode ``pygit2.Oid`` as PostgreSQL ``bytea`` (text parameter format)."""

    oid = _oids.BYTEA_OID

    def __init__(self, cls: type, context: AdaptContext | None = None):
        super().__init__(cls, context)
        self._bytes = BytesDumper(bytes, context)

    def dump(self, obj: pygit2.Oid) -> Buffer | None:
        return self._bytes.dump(obj.raw)


class OidBinaryDumper(Dumper):
    """Encode ``pygit2.Oid`` as PostgreSQL ``bytea`` (binary parameter format)."""

    format = Format.BINARY
    oid = _oids.BYTEA_OID

    def dump(self, obj: pygit2.Oid) -> Buffer | None:
        return obj.raw


def register_adapters(
    conn: Connection[Any],
    *,
    git_object_type_regtype: str = "git_object_type",
) -> None:
    """
    Register dumpers/loaders on ``conn`` for ``pygit2.Oid`` and ``git_object_type``.

    Call once per sync connection after the schema exists (enum type present).

    :param git_object_type_regtype: argument to :meth:`EnumInfo.fetch` (e.g.
        :func:`regtype_git_object_type` if the type is not on ``search_path``).
    """
    info = EnumInfo.fetch(conn, git_object_type_regtype)
    if info is None:
        raise LookupError(
            "PostgreSQL type git_object_type not found; apply schema.sql first "
            "(use the regtype your search_path resolves, e.g. public.git_object_type)."
        )
    register_enum(
        info,
        conn,
        enum=GitObjectType,
        mapping=_GIT_OBJECT_TYPE_PG_MAP,
    )
    conn.adapters.register_dumper(pygit2.Oid, OidDumper)
    conn.adapters.register_dumper(pygit2.Oid, OidBinaryDumper)


async def register_adapters_async(
    conn: AsyncConnection[Any],
    *,
    git_object_type_regtype: str = "git_object_type",
) -> None:
    """Async variant of :func:`register_adapters`."""
    info = await EnumInfo.fetch(conn, git_object_type_regtype)
    if info is None:
        raise LookupError(
            "PostgreSQL type git_object_type not found; apply schema.sql first."
        )
    register_enum(
        info,
        conn,
        enum=GitObjectType,
        mapping=_GIT_OBJECT_TYPE_PG_MAP,
    )
    conn.adapters.register_dumper(pygit2.Oid, OidDumper)
    conn.adapters.register_dumper(pygit2.Oid, OidBinaryDumper)


def oid_from_db(value: bytes | memoryview) -> pygit2.Oid:
    """Build ``Oid`` from a ``bytea`` value (e.g. manual handling without a row factory)."""
    return pygit2.Oid(raw=bytes(value))


def postgit_tuple_row(cursor: BaseCursor[Any, Any]) -> RowMaker[tuple[Any, ...]]:
    """
    Like :func:`psycopg.rows.tuple_row`, but turns ``oid`` and ``target_oid``
    columns into ``pygit2.Oid``.
    """
    if not cursor.description:
        return tuple_row(cursor)
    names = [d.name for d in cursor.description]
    idxs = [i for i, n in enumerate(names) if n in _OID_COLUMN_NAMES]
    if not idxs:
        return tuple_row(cursor)

    def rowmaker(values: Sequence[Any]) -> tuple[Any, ...]:
        out = list(values)
        for i in idxs:
            v = out[i]
            if isinstance(v, (bytes, memoryview)):
                out[i] = oid_from_db(v)
        return tuple(out)

    return rowmaker


def postgit_dict_row(cursor: BaseCursor[Any, Any]) -> RowMaker[dict[str, Any]]:
    """
    Like :func:`psycopg.rows.dict_row`, but turns ``oid`` and ``target_oid``
    values into ``pygit2.Oid``.
    """
    inner = dict_row(cursor)
    if cursor.description is None:
        return inner
    names = [d.name for d in cursor.description]
    idxs = [i for i, n in enumerate(names) if n in _OID_COLUMN_NAMES]
    if not idxs:
        return inner

    def rowmaker(values: Sequence[Any]) -> dict[str, Any]:
        d = dict(zip(names, values))
        for k in _OID_COLUMN_NAMES & d.keys():
            v = d[k]
            if isinstance(v, (bytes, memoryview)):
                d[k] = oid_from_db(v)
        return d

    return rowmaker


def ref_storage_columns(
    ref: pygit2.Reference,
) -> tuple[pygit2.Oid | None, str | None]:
    """
    Values for ``(target_oid, symbolic_target)`` on the ``refs`` table.

    Matches :func:`register_adapters` expectations: direct refs supply ``Oid``;
    symbolic refs supply the target ref name as ``str``.
    """
    if ref.type == _GIT_REFERENCE_SYMBOLIC:
        t = ref.target
        if not isinstance(t, str):
            t = str(t)
        return None, t
    if ref.type == _GIT_REFERENCE_OID:
        tgt = ref.target
        if not isinstance(tgt, pygit2.Oid):
            tgt = pygit2.Oid(hex=str(tgt))
        return tgt, None
    raise TypeError(f"unsupported reference type: {ref.type!r}")


def object_type_from_pygit2(obj: pygit2.Object) -> GitObjectType:
    """Map ``Object.type`` (libgit2 kind int) to :class:`GitObjectType`."""
    return GitObjectType(obj.type)


def object_data_from_pygit2(obj: pygit2.Object) -> bytes:
    """Object payload for ``objects.object_data`` (same as ``Object.read_raw()``)."""
    return obj.read_raw()


def objects_row_insert(
    obj: pygit2.Object,
) -> tuple[pygit2.Oid, GitObjectType, bytes]:
    """``(oid, object_type, object_data)`` for inserting into ``objects``."""
    return obj.id, object_type_from_pygit2(obj), object_data_from_pygit2(obj)


def regtype_git_object_type(schema: str = "public") -> str:
    """Qualified type name for :func:`EnumInfo.fetch` if ``search_path`` is not set."""
    return f"{schema}.git_object_type"