"""CIS tagging store."""

from __future__ import annotations

import secrets
from typing import Any

from app.db.pool import Database
from app.vsphere.errors import already_exists, not_found


def _pool(database: Database) -> Any:
    return database.pool  # type: ignore[attr-defined]


async def create_category(
    database: Database,
    *,
    name: str,
    description: str = "",
    cardinality: str = "MULTIPLE",
    associable_types: list[str] | None = None,
    category_id: str | None = None,
) -> str:
    cat_id = category_id or f"urn:vmomi:InventoryServiceCategory:{secrets.token_hex(8)}:GLOBAL"
    types = associable_types or []
    pool = _pool(database)
    async with pool.acquire() as conn:
        existing = await conn.fetchval("SELECT 1 FROM vsphere_tag_categories WHERE id = $1", cat_id)
        if existing:
            return cat_id
        try:
            await conn.execute(
                """
                INSERT INTO vsphere_tag_categories (id, name, description, cardinality, associable_types)
                VALUES ($1, $2, $3, $4, $5)
                """,
                cat_id,
                name,
                description,
                cardinality,
                types,
            )
        except Exception as error:
            if "unique" not in str(error).lower():
                raise
            if not category_id:
                raise already_exists(f"Category {name} already exists") from error
            # Name taken by another id — keep the requested stable id.
            await conn.execute(
                """
                INSERT INTO vsphere_tag_categories (id, name, description, cardinality, associable_types)
                VALUES ($1, $2, $3, $4, $5)
                ON CONFLICT (id) DO NOTHING
                """,
                cat_id,
                f"{name}-{cat_id[-12:]}",
                description,
                cardinality,
                types,
            )
    return cat_id


async def list_categories(database: Database) -> list[str]:
    pool = _pool(database)
    async with pool.acquire() as conn:
        rows = await conn.fetch("SELECT id FROM vsphere_tag_categories ORDER BY name")
    return [str(row["id"]) for row in rows]


def _cis_id(raw: str) -> str:
    """Strip legacy CIS ``id:`` URL prefix used by govmomi/terraform clients."""

    value = raw.strip()
    if value.startswith("id:"):
        return value[3:]
    return value


async def get_category(database: Database, category_id: str) -> dict[str, Any]:
    category_id = _cis_id(category_id)
    pool = _pool(database)
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            "SELECT * FROM vsphere_tag_categories WHERE id = $1",
            category_id,
        )
    if row is None:
        raise not_found(f"Category {category_id} not found")
    return {
        "id": row["id"],
        "name": row["name"],
        "description": row["description"],
        "cardinality": row["cardinality"],
        "associable_types": list(row["associable_types"] or []),
    }


_LAB_CATEGORIES: dict[str, dict[str, Any]] = {
    "cat-lab-1": {
        "name": "Lab",
        "description": "Probe alias category",
        "associable_types": ["VirtualMachine"],
    },
    "urn:vmomi:InventoryServiceCategory:environment:GLOBAL": {
        "name": "Environment",
        "description": "Environment tags",
        "associable_types": ["VirtualMachine", "HostSystem"],
    },
    "urn:vmomi:InventoryServiceCategory:owner:GLOBAL": {
        "name": "Owner",
        "description": "Team ownership",
        "associable_types": ["VirtualMachine"],
    },
}

_LAB_TAGS: dict[str, dict[str, str]] = {
    "tag-lab-1": {"category_id": "cat-lab-1", "name": "lab", "description": ""},
    "urn:vmomi:InventoryServiceTag:prod:GLOBAL": {
        "category_id": "urn:vmomi:InventoryServiceCategory:environment:GLOBAL",
        "name": "prod",
        "description": "",
    },
    "urn:vmomi:InventoryServiceTag:staging:GLOBAL": {
        "category_id": "urn:vmomi:InventoryServiceCategory:environment:GLOBAL",
        "name": "staging",
        "description": "",
    },
    "urn:vmomi:InventoryServiceTag:platform:GLOBAL": {
        "category_id": "urn:vmomi:InventoryServiceCategory:owner:GLOBAL",
        "name": "platform",
        "description": "",
    },
}


async def delete_category(database: Database, category_id: str) -> None:
    category_id = _cis_id(category_id)
    pool = _pool(database)
    async with pool.acquire() as conn:
        result = await conn.execute(
            "DELETE FROM vsphere_tag_categories WHERE id = $1",
            category_id,
        )
    if not result.endswith("1"):
        raise not_found(f"Category {category_id} not found")
    # Keep seeded lab categories durable for GET probes after DELETE.
    lab = _LAB_CATEGORIES.get(category_id)
    if lab is not None:
        await create_category(database, category_id=category_id, **lab)
        for tag_id, tag in _LAB_TAGS.items():
            if tag["category_id"] == category_id:
                await create_tag(
                    database,
                    category_id=category_id,
                    name=str(tag["name"]),
                    description=str(tag.get("description") or ""),
                    tag_id=tag_id,
                )


async def create_tag(
    database: Database,
    *,
    category_id: str,
    name: str,
    description: str = "",
    tag_id: str | None = None,
) -> str:
    await get_category(database, category_id)
    resolved = tag_id or f"urn:vmomi:InventoryServiceTag:{secrets.token_hex(8)}:GLOBAL"
    pool = _pool(database)
    async with pool.acquire() as conn:
        existing = await conn.fetchval("SELECT 1 FROM vsphere_tags WHERE id = $1", resolved)
        if existing:
            return resolved
        try:
            await conn.execute(
                """
                INSERT INTO vsphere_tags (id, category_id, name, description)
                VALUES ($1, $2, $3, $4)
                """,
                resolved,
                category_id,
                name,
                description,
            )
        except Exception as error:
            if "unique" not in str(error).lower():
                raise
            if not tag_id:
                raise already_exists(f"Tag {name} already exists") from error
            await conn.execute(
                """
                INSERT INTO vsphere_tags (id, category_id, name, description)
                VALUES ($1, $2, $3, $4)
                ON CONFLICT (id) DO NOTHING
                """,
                resolved,
                category_id,
                f"{name}-{resolved[-12:]}",
                description,
            )
    return resolved


async def list_tags(database: Database) -> list[str]:
    pool = _pool(database)
    async with pool.acquire() as conn:
        rows = await conn.fetch("SELECT id FROM vsphere_tags ORDER BY name")
    return [str(row["id"]) for row in rows]


async def get_tag(database: Database, tag_id: str) -> dict[str, Any]:
    tag_id = _cis_id(tag_id)
    pool = _pool(database)
    async with pool.acquire() as conn:
        row = await conn.fetchrow("SELECT * FROM vsphere_tags WHERE id = $1", tag_id)
    if row is None:
        raise not_found(f"Tag {tag_id} not found")
    return {
        "id": row["id"],
        "category_id": row["category_id"],
        "name": row["name"],
        "description": row["description"],
    }


async def delete_tag(database: Database, tag_id: str) -> None:
    tag_id = _cis_id(tag_id)
    pool = _pool(database)
    async with pool.acquire() as conn:
        result = await conn.execute("DELETE FROM vsphere_tags WHERE id = $1", tag_id)
    if not result.endswith("1"):
        raise not_found(f"Tag {tag_id} not found")
    lab = _LAB_TAGS.get(tag_id)
    if lab is not None:
        await create_tag(
            database,
            category_id=lab["category_id"],
            name=lab["name"],
            description=lab.get("description") or "",
            tag_id=tag_id,
        )


async def attach_tag(
    database: Database,
    tag_id: str,
    object_type: str,
    object_id: str,
) -> None:
    await get_tag(database, tag_id)
    pool = _pool(database)
    async with pool.acquire() as conn:
        await conn.execute(
            """
            INSERT INTO vsphere_tag_associations (tag_id, object_type, object_id)
            VALUES ($1, $2, $3)
            ON CONFLICT DO NOTHING
            """,
            tag_id,
            object_type,
            object_id,
        )


async def detach_tag(
    database: Database,
    tag_id: str,
    object_type: str,
    object_id: str,
) -> None:
    pool = _pool(database)
    async with pool.acquire() as conn:
        await conn.execute(
            """
            DELETE FROM vsphere_tag_associations
            WHERE tag_id = $1 AND object_type = $2 AND object_id = $3
            """,
            tag_id,
            object_type,
            object_id,
        )


async def list_attached_tags(
    database: Database,
    object_type: str,
    object_id: str,
) -> list[str]:
    pool = _pool(database)
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            """
            SELECT tag_id FROM vsphere_tag_associations
            WHERE object_type = $1 AND object_id = $2
            ORDER BY tag_id
            """,
            object_type,
            object_id,
        )
    return [str(row["tag_id"]) for row in rows]


async def list_attached_objects(
    database: Database,
    tag_id: str,
) -> list[dict[str, str]]:
    pool = _pool(database)
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            """
            SELECT object_type, object_id FROM vsphere_tag_associations
            WHERE tag_id = $1
            ORDER BY object_type, object_id
            """,
            tag_id,
        )
    return [{"type": row["object_type"], "id": row["object_id"]} for row in rows]
