"""GDB database tools — list, get, compare, create, update, edit, delete, duplicate."""

from __future__ import annotations

from typing import Any

from fastmcp.exceptions import ToolError
from mcp.types import ToolAnnotations

from mcp_eregistrations_gdb.gdb_client import GDBClient, GDBClientError, translate_error
from mcp_eregistrations_gdb.server import mcp
from mcp_eregistrations_gdb.tools import _add_optional


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def gdb_database_list(instance: str | None = None) -> dict[str, Any]:
    """List all databases across all catalogs (flattened).

    Each database includes its parent catalog_code, catalog_name, catalog_id.
    For full catalog metadata (order, group, data_index_increment), use
    gdb_catalog_list instead.

    Args:
        instance: Instance name (default: auto-select).

    Returns:
        dict with count, databases.
    """
    try:
        async with GDBClient.for_instance(instance) as client:
            catalogs = await client.get("/databases")
            if not isinstance(catalogs, list):
                catalogs = catalogs.get("catalogs", [])
            databases: list[dict[str, Any]] = []
            for catalog in catalogs:
                for db in catalog.get("databases", []):
                    databases.append(
                        {
                            "id": db.get("id"),
                            "uuid": db.get("uuid"),
                            "version": db.get("version"),
                            "name": db.get("name"),
                            "is_draft": db.get("is_draft"),
                            "catalog_code": catalog.get("code"),
                            "catalog_name": catalog.get("name"),
                            "catalog_id": catalog.get("id"),
                        }
                    )
            return {"count": len(databases), "databases": databases}
    except GDBClientError as e:
        raise translate_error(e, resource_type="database") from e


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def gdb_database_get(
    database_id: int,
    instance: str | None = None,
) -> dict[str, Any]:
    """Get a single database with full JSON Schema.

    Args:
        database_id: Database ID (integer).
        instance: Instance name (default: auto-select).

    Returns:
        dict with id, code, version, json_schema, catalog info.
    """
    try:
        async with GDBClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.get(f"/database/{database_id}")
            return result
    except GDBClientError as e:
        raise translate_error(
            e, resource_type="database", resource_id=database_id
        ) from e


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def gdb_database_compare(
    first_database_id: int | None = None,
    first_database_code: str | None = None,
    first_database_version: float | None = None,
    second_database_id: int | None = None,
    second_database_code: str | None = None,
    second_database_version: float | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Compare two databases to find field differences.

    Identify by ID or by code+version for each database.

    Args:
        first_database_id: First database ID (use OR code+version).
        first_database_code: First database code (requires version).
        first_database_version: First database version (requires code).
        second_database_id: Second database ID (use OR code+version).
        second_database_code: Second database code (requires version).
        second_database_version: Second database version (requires code).
        instance: Instance name (default: auto-select).

    Returns:
        dict with is_equals, first_database_missing_fields,
        second_database_missing_fields.
    """
    # Validate first database params
    has_first_id = first_database_id is not None
    has_first_code = (
        first_database_code is not None and first_database_version is not None
    )
    if not has_first_id and not has_first_code:
        raise ToolError(
            "Provide first_database_id OR "
            "(first_database_code + first_database_version)."
        )

    # Validate second database params
    has_second_id = second_database_id is not None
    has_second_code = (
        second_database_code is not None and second_database_version is not None
    )
    if not has_second_id and not has_second_code:
        raise ToolError(
            "Provide second_database_id OR "
            "(second_database_code + second_database_version)."
        )

    # Build request body
    body: dict[str, Any] = {}
    if has_first_id:
        body["first_database_id"] = first_database_id
    else:
        body["first_database_code"] = first_database_code
        body["first_database_version"] = first_database_version

    if has_second_id:
        body["second_database_id"] = second_database_id
    else:
        body["second_database_code"] = second_database_code
        body["second_database_version"] = second_database_version

    try:
        async with GDBClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.post(
                "/api/v1/database/compare", data=body
            )
            return result
    except GDBClientError as e:
        raise translate_error(e, resource_type="database comparison") from e


def _build_modify_payload(
    *,
    database_id: int | None = None,
    catalog_name: str | None = None,
    code: str | None = None,
    schema: dict[str, Any] | None = None,
    is_draft: bool | None = None,
    number_format: str | None = None,
    name: str | None = None,
    description: str | None = None,
    institution: str | None = None,
    group_id: int | None = None,
    group_name: str | None = None,
    color_hex: str | None = None,
    schema_tags: list[dict[str, Any]] | None = None,
    schema_flags: list[dict[str, Any]] | None = None,
    fields_uniques: list[list[int]] | None = None,
    order_fields: list[str] | None = None,
    indexes: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Build the POST /api/v1/database/modify payload.

    Only includes optional fields when they are not None.
    GDB API uses a MIX of snake_case and camelCase (matches frontend Formik form):
    - snake_case: catalog_name, code, name, description, institution, color_hex
    - camelCase: isDraft, numberFormat, schemaTags, schemaFlags
    - snake_case: group_id, group_name, schema, fields_uniques, order_fields
    """
    payload: dict[str, Any] = {}
    _add_optional(payload, "id", database_id)
    _add_optional(payload, "catalog_name", catalog_name)
    _add_optional(payload, "code", code)
    _add_optional(payload, "schema", schema)
    _add_optional(payload, "isDraft", is_draft)
    _add_optional(payload, "numberFormat", number_format)
    _add_optional(payload, "name", name)
    _add_optional(payload, "description", description)
    _add_optional(payload, "institution", institution)
    _add_optional(payload, "group_id", group_id)
    _add_optional(payload, "group_name", group_name)
    _add_optional(payload, "color_hex", color_hex)
    _add_optional(payload, "schemaTags", schema_tags)
    _add_optional(payload, "schemaFlags", schema_flags)
    _add_optional(payload, "fields_uniques", fields_uniques)
    _add_optional(payload, "order_fields", order_fields)
    _add_optional(payload, "indexes", indexes)
    return payload


@mcp.tool()
async def gdb_database_create(
    catalog_name: str,
    code: str,
    schema: dict[str, Any],
    is_draft: bool = True,
    number_format: str = "{code}{indexNoByCode}",
    name: str | None = None,
    description: str | None = None,
    institution: str | None = None,
    group_id: int | None = None,
    group_name: str | None = None,
    color_hex: str | None = None,
    schema_tags: list[dict[str, Any]] | None = None,
    schema_flags: list[dict[str, Any]] | None = None,
    fields_uniques: list[list[int]] | None = None,
    order_fields: list[str] | None = None,
    indexes: dict[str, Any] | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Create a new GDB database.

    group_id and group_name are mutually exclusive.

    Args:
        catalog_name: Catalog name for the database.
        code: Unique database code.
        schema: JSON Schema defining the database fields.
        is_draft: Create as draft (default: True).
        number_format: Number format pattern (default: "{code}{indexNoByCode}").
        name: Human-readable name.
        description: Database description.
        institution: Institution name.
        group_id: Existing group ID (mutually exclusive with group_name).
        group_name: New group name (mutually exclusive with group_id).
        color_hex: Color hex code.
        schema_tags: Tag definitions for database fields.
        schema_flags: Flag definitions for database fields.
        fields_uniques: Unique constraint groups (list of field index lists).
        order_fields: Field names used for default ordering.
        indexes: Index definitions for the database.
        instance: Instance name (default: auto-select).

    Returns:
        dict with id, catalog_id, catalog_code, catalog_data_index_increment,
        group_id.
    """
    if group_id is not None and group_name is not None:
        raise ToolError(
            "group_id and group_name are mutually exclusive. "
            "Use group_id to assign to an existing group, "
            "or group_name to create a new group."
        )

    payload = _build_modify_payload(
        catalog_name=catalog_name,
        code=code,
        schema=schema,
        is_draft=is_draft,
        number_format=number_format,
        name=name,
        description=description,
        institution=institution,
        group_id=group_id,
        group_name=group_name,
        color_hex=color_hex,
        schema_tags=schema_tags,
        schema_flags=schema_flags,
        fields_uniques=fields_uniques,
        order_fields=order_fields,
        indexes=indexes,
    )

    try:
        async with GDBClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.post(
                "/api/v1/database/modify", data=payload
            )
            return result
    except GDBClientError as e:
        raise translate_error(e, resource_type="database") from e


@mcp.tool()
async def gdb_database_update(
    database_id: int,
    schema: dict[str, Any] | None = None,
    catalog_name: str | None = None,
    code: str | None = None,
    is_draft: bool | None = None,
    number_format: str | None = None,
    name: str | None = None,
    description: str | None = None,
    institution: str | None = None,
    group_id: int | None = None,
    group_name: str | None = None,
    color_hex: str | None = None,
    schema_tags: list[dict[str, Any]] | None = None,
    schema_flags: list[dict[str, Any]] | None = None,
    fields_uniques: list[list[int]] | None = None,
    order_fields: list[str] | None = None,
    indexes: dict[str, Any] | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Update an existing draft GDB database.

    Only draft databases can be updated. group_id and group_name are
    mutually exclusive. Schema is optional — omit it for metadata-only updates.

    Args:
        database_id: Database ID to update.
        schema: Updated JSON Schema (optional for metadata-only updates).
        catalog_name: Catalog name.
        code: Database code.
        is_draft: Draft status.
        number_format: Number format pattern.
        name: Human-readable name.
        description: Database description.
        institution: Institution name.
        group_id: Existing group ID (mutually exclusive with group_name).
        group_name: New group name (mutually exclusive with group_id).
        color_hex: Color hex code.
        schema_tags: Tag definitions for database fields.
        schema_flags: Flag definitions for database fields.
        fields_uniques: Unique constraint groups (list of field index lists).
        order_fields: Field names used for default ordering.
        indexes: Index definitions for the database.
        instance: Instance name (default: auto-select).

    Returns:
        dict with id, catalog_id, catalog_code, catalog_data_index_increment,
        group_id.
    """
    if group_id is not None and group_name is not None:
        raise ToolError(
            "group_id and group_name are mutually exclusive. "
            "Use group_id to assign to an existing group, "
            "or group_name to create a new group."
        )

    payload = _build_modify_payload(
        database_id=database_id,
        catalog_name=catalog_name,
        code=code,
        schema=schema,
        is_draft=is_draft,
        number_format=number_format,
        name=name,
        description=description,
        institution=institution,
        group_id=group_id,
        group_name=group_name,
        color_hex=color_hex,
        schema_tags=schema_tags,
        schema_flags=schema_flags,
        fields_uniques=fields_uniques,
        order_fields=order_fields,
        indexes=indexes,
    )

    try:
        async with GDBClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.post(
                "/api/v1/database/modify", data=payload
            )
            return result
    except GDBClientError as e:
        raise translate_error(
            e, resource_type="database", resource_id=database_id
        ) from e


@mcp.tool()
async def gdb_database_edit(
    database_id: int,
    instance: str | None = None,
) -> dict[str, Any]:
    """Create an editable draft from a published database.

    Returns only the new draft's {id}. Call gdb_database_get(id) to fetch
    the full database with schema.

    Args:
        database_id: Database ID to create a draft from.
        instance: Instance name (default: auto-select).

    Returns:
        dict with id of the new draft database.
    """
    try:
        async with GDBClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.post(
                f"/api/v1/database/{database_id}/edit", data={}
            )
            return result
    except GDBClientError as e:
        raise translate_error(
            e, resource_type="database", resource_id=database_id
        ) from e


@mcp.tool()
async def gdb_database_publish(
    database_id: int,
    instance: str | None = None,
) -> dict[str, Any]:
    """Publish a draft database (makes it immutable and usable by services).

    Only draft databases can be published. Once published, the schema is
    locked and data views/services can reference it. Fetches current state
    first since /modify requires catalog_name, code, and schema.

    Args:
        database_id: Draft database ID to publish.
        instance: Instance name (default: auto-select).

    Returns:
        dict with id, catalog_id, catalog_code.
    """
    try:
        async with GDBClient.for_instance(instance) as client:
            # /modify requires catalog_name, code, schema on every call.
            # GET /database/{id} returns name + code at top level (= catalog fields).
            db: dict[str, Any] = await client.get(f"/database/{database_id}")
            if not db.get("is_draft"):
                raise ToolError(f"Database {database_id} is already published.")
            payload = _build_modify_payload(
                database_id=database_id,
                catalog_name=db["name"],
                code=db["code"],
                schema=db["schema"],
                is_draft=False,
            )
            result: dict[str, Any] = await client.post(
                "/api/v1/database/modify", data=payload
            )
            return result
    except GDBClientError as e:
        raise translate_error(
            e, resource_type="database", resource_id=database_id
        ) from e


@mcp.tool()
async def gdb_database_delete(
    database_uuid: str,
    instance: str | None = None,
) -> dict[str, Any]:
    """Delete a GDB database by UUID.

    Args:
        database_uuid: Database UUID to delete.
        instance: Instance name (default: auto-select).

    Returns:
        dict with deleted (bool) and database_uuid.
    """
    try:
        async with GDBClient.for_instance(instance) as client:
            await client.delete(f"/api/v1/database/{database_uuid}")
            return {"deleted": True, "database_uuid": database_uuid}
    except GDBClientError as e:
        raise translate_error(
            e, resource_type="database", resource_id=database_uuid
        ) from e


@mcp.tool()
async def gdb_database_duplicate(
    database_id: int,
    copy_data: bool = False,
    instance: str | None = None,
) -> dict[str, Any]:
    """Duplicate a GDB database.

    Args:
        database_id: Database ID to duplicate.
        copy_data: Whether to copy data rows (default: False).
        instance: Instance name (default: auto-select).

    Returns:
        dict with id, catalog_id, code, name, catalog_data_index_increment,
        group_id.
    """
    try:
        async with GDBClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.post(
                f"/api/v1/database/{database_id}/duplicate",
                data={"copyData": copy_data},
            )
            return result
    except GDBClientError as e:
        raise translate_error(
            e, resource_type="database", resource_id=database_id
        ) from e
