"""Content library + datastore file metadata."""

from __future__ import annotations

import secrets
from typing import Any

from asyncpg.exceptions import UniqueViolationError  # type: ignore[import-untyped]

from app.db.pool import Database
from app.vsphere import inventory
from app.vsphere.domain import tagging
from app.vsphere.domain import tasks as task_store
from app.vsphere.errors import already_exists, invalid_argument, not_found


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


async def create_library(
    database: Database,
    *,
    name: str,
    description: str = "",
    library_id: str | None = None,
) -> str:
    if not name.strip():
        raise invalid_argument("name is required")
    lib_id = library_id or f"lib-{secrets.token_hex(6)}"
    pool = _pool(database)
    try:
        async with pool.acquire() as conn:
            await conn.execute(
                """
                INSERT INTO vsphere_libraries (id, name, description, type, props)
                VALUES ($1, $2, $3, 'LOCAL', '{}'::jsonb)
                ON CONFLICT (id) DO NOTHING
                """,
                lib_id,
                name,
                description,
            )
            # Id already present (seed ensure) — treat as success.
            exists = await conn.fetchval("SELECT 1 FROM vsphere_libraries WHERE id = $1", lib_id)
            if exists:
                return lib_id
    except UniqueViolationError as error:
        # Name collision with a different id: keep requesting id with a unique name.
        if library_id:
            pool = _pool(database)
            async with pool.acquire() as conn:
                await conn.execute(
                    """
                    INSERT INTO vsphere_libraries (id, name, description, type, props)
                    VALUES ($1, $2, $3, 'LOCAL', '{}'::jsonb)
                    ON CONFLICT (id) DO NOTHING
                    """,
                    lib_id,
                    f"{name} ({lib_id})",
                    description,
                )
            return lib_id
        raise already_exists(f"Library {name} already exists") from error
    return lib_id


def _props(value: Any) -> dict[str, Any]:
    if isinstance(value, dict):
        return value
    if isinstance(value, str):
        import json

        try:
            decoded = json.loads(value)
        except json.JSONDecodeError:
            return {}
        return decoded if isinstance(decoded, dict) else {}
    return {}


def _library_info(row: Any) -> dict[str, Any]:
    props = _props(row["props"])
    return {
        "id": row["id"],
        "name": row["name"],
        "description": row["description"] or "",
        "type": row["type"],
        "creation_time": props.get("creation_time"),
        "last_modified_time": props.get("last_modified_time"),
        "storage_backings": props.get("storage_backings")
        or [{"type": "DATASTORE", "datastore_id": "datastore-31"}],
        "state": props.get("state") or "ACTIVE",
        "version": str(props.get("version") or "1"),
    }


def _library_item_info(row: Any) -> dict[str, Any]:
    props = _props(row["props"])
    return {
        "id": row["id"],
        "library_id": row["library_id"],
        "name": row["name"],
        "type": row["type"],
        "description": row["description"] or "",
        "content_version": str(props.get("content_version") or "1"),
        "creation_time": props.get("creation_time"),
        "last_modified_time": props.get("last_modified_time"),
        "size": int(props.get("size") or 0),
        "cached": bool(props.get("cached", True)),
        "security_compliance": bool(props.get("security_compliance", True)),
    }


async def list_libraries(database: Database) -> list[dict[str, Any]]:
    pool = _pool(database)
    async with pool.acquire() as conn:
        rows = await conn.fetch("SELECT * FROM vsphere_libraries ORDER BY name")
    return [_library_info(row) for row in rows]


async def get_library(database: Database, library_id: str) -> dict[str, Any]:
    pool = _pool(database)
    async with pool.acquire() as conn:
        row = await conn.fetchrow("SELECT * FROM vsphere_libraries WHERE id = $1", library_id)
    if row is None:
        raise not_found(f"Library {library_id} not found")
    return _library_info(row)


async def delete_library(database: Database, library_id: str) -> None:
    pool = _pool(database)
    async with pool.acquire() as conn:
        result = await conn.execute("DELETE FROM vsphere_libraries WHERE id = $1", library_id)
    if result == "DELETE 0":
        raise not_found(f"Library {library_id} not found")


async def get_library_item(database: Database, item_id: str) -> dict[str, Any]:
    pool = _pool(database)
    async with pool.acquire() as conn:
        row = await conn.fetchrow("SELECT * FROM vsphere_library_items WHERE id = $1", item_id)
    if row is None:
        raise not_found(f"Library item {item_id} not found")
    return _library_item_info(row)


async def delete_library_item(database: Database, item_id: str) -> None:
    pool = _pool(database)
    async with pool.acquire() as conn:
        result = await conn.execute("DELETE FROM vsphere_library_items WHERE id = $1", item_id)
    if result == "DELETE 0":
        raise not_found(f"Library item {item_id} not found")


async def create_library_item(
    database: Database,
    *,
    library_id: str,
    name: str,
    item_type: str = "ovf",
    description: str = "",
    item_id: str | None = None,
) -> str:
    pool = _pool(database)
    async with pool.acquire() as conn:
        exists = await conn.fetchval("SELECT 1 FROM vsphere_libraries WHERE id = $1", library_id)
        if not exists:
            raise not_found(f"Library {library_id} not found")
        resolved_id = item_id or f"item-{secrets.token_hex(6)}"
        try:
            await conn.execute(
                """
                INSERT INTO vsphere_library_items (id, library_id, name, type, description)
                VALUES ($1, $2, $3, $4, $5)
                ON CONFLICT (id) DO NOTHING
                """,
                resolved_id,
                library_id,
                name,
                item_type,
                description,
            )
        except UniqueViolationError:
            # (library_id, name) taken — keep stable id with a unique name for seed.
            if item_id:
                await conn.execute(
                    """
                    INSERT INTO vsphere_library_items (id, library_id, name, type, description)
                    VALUES ($1, $2, $3, $4, $5)
                    ON CONFLICT (id) DO NOTHING
                    """,
                    resolved_id,
                    library_id,
                    f"{name}-{resolved_id}",
                    item_type,
                    description,
                )
            else:
                raise
    return resolved_id


async def list_library_items(database: Database, library_id: str) -> list[dict[str, Any]]:
    pool = _pool(database)
    async with pool.acquire() as conn:
        exists = await conn.fetchval("SELECT 1 FROM vsphere_libraries WHERE id = $1", library_id)
        if not exists:
            raise not_found(f"Library {library_id} not found")
        rows = await conn.fetch(
            "SELECT * FROM vsphere_library_items WHERE library_id = $1 ORDER BY name",
            library_id,
        )
    return [_library_item_info(row) for row in rows]


_LAB_SESSION_ID = "session-lab-1"
_LAB_ITEM_ID = "item-ubuntu"


async def clear_transfer_sessions(database: Database) -> None:
    """Drop durable transfer sessions (used by inventory reseed)."""

    pool = _pool(database)
    async with pool.acquire() as conn:
        await conn.execute(
            """
            DO $$ BEGIN
              IF to_regclass('public.vsphere_transfer_sessions') IS NOT NULL THEN
                DELETE FROM vsphere_transfer_sessions;
              END IF;
            END $$;
            """
        )


def _decode_files(value: Any) -> dict[str, Any]:
    import json

    current = value
    while isinstance(current, str):
        current = json.loads(current)
    if isinstance(current, dict):
        return current
    return {}


async def _upsert_session(
    database: Database,
    *,
    session_id: str,
    kind: str,
    library_item_id: str,
    state: str,
    files: dict[str, Any],
) -> None:
    import json

    pool = _pool(database)
    async with pool.acquire() as conn:
        await conn.execute(
            """
            INSERT INTO vsphere_transfer_sessions (id, kind, library_item_id, state, files, updated_at)
            VALUES ($1, $2, $3, $4, $5::jsonb, now())
            ON CONFLICT (id, kind) DO UPDATE SET
              library_item_id = EXCLUDED.library_item_id,
              state = EXCLUDED.state,
              files = EXCLUDED.files,
              updated_at = now()
            """,
            session_id,
            kind,
            library_item_id,
            state,
            json.dumps(files),
        )


async def _get_session_row(
    database: Database, session_id: str, kind: str | None = None
) -> dict[str, Any] | None:
    pool = _pool(database)
    async with pool.acquire() as conn:
        if kind:
            row = await conn.fetchrow(
                "SELECT * FROM vsphere_transfer_sessions WHERE id = $1 AND kind = $2",
                session_id,
                kind,
            )
        else:
            row = await conn.fetchrow(
                "SELECT * FROM vsphere_transfer_sessions WHERE id = $1",
                session_id,
            )
    if row is None:
        return None
    return {
        "id": row["id"],
        "kind": row["kind"],
        "library_item_id": row["library_item_id"],
        "state": row["state"],
        "files": _decode_files(row["files"]),
    }


async def create_update_session(
    database: Database,
    *,
    library_item_id: str,
    session_id: str | None = None,
) -> str:
    pool = _pool(database)
    async with pool.acquire() as conn:
        exists = await conn.fetchval(
            "SELECT 1 FROM vsphere_library_items WHERE id = $1", library_item_id
        )
    if not exists:
        raise not_found(f"Library item {library_item_id} not found")
    resolved = session_id or f"update-session-{secrets.token_hex(6)}"
    files = {
        "upload.bin": {
            "name": "upload.bin",
            "source_type": "PUSH",
            "size": 1024,
            "status": "READY",
            "upload_endpoint": {
                "uri": f"/api/content/library/item/update-session/{resolved}/file/upload.bin",
            },
        }
    }
    await _upsert_session(
        database,
        session_id=resolved,
        kind="update",
        library_item_id=library_item_id,
        state="ACTIVE",
        files=files,
    )
    return resolved


async def add_update_session_file(
    database: Database,
    session_id: str,
    *,
    name: str,
    source_type: str = "PUSH",
    size: int = 0,
    content: str = "",
) -> dict[str, Any]:
    session = await _get_session_row(database, session_id, "update")
    if session is None:
        raise not_found(f"Update session {session_id} not found")
    file_info = {
        "name": name,
        "source_type": source_type,
        "size": size or len(content.encode("utf-8")),
        "status": "READY",
        "upload_endpoint": {
            "uri": f"/api/content/library/item/update-session/{session_id}/file/{name}",
        },
        "content": content,
    }
    files = dict(session["files"])
    files[name] = file_info
    await _upsert_session(
        database,
        session_id=session_id,
        kind="update",
        library_item_id=session["library_item_id"],
        state=session["state"],
        files=files,
    )
    return {
        "name": name,
        "source_type": source_type,
        "size": file_info["size"],
        "status": "READY",
        "upload_endpoint": file_info["upload_endpoint"],
    }


async def complete_update_session(database: Database, session_id: str) -> None:
    session = await _get_session_row(database, session_id, "update")
    if session is None:
        raise not_found(f"Update session {session_id} not found")
    await _upsert_session(
        database,
        session_id=session_id,
        kind="update",
        library_item_id=session["library_item_id"],
        state="DONE",
        files=session["files"],
    )


async def get_update_session(database: Database, session_id: str) -> dict[str, Any]:
    session = await _get_session_row(database, session_id, "update")
    if session is None:
        raise not_found(f"Update session {session_id} not found")
    return {
        "id": session["id"],
        "library_item_id": session["library_item_id"],
        "state": session["state"],
        "client_progress": 100 if session["state"] == "DONE" else 50,
    }


async def create_download_session(
    database: Database,
    *,
    library_item_id: str,
    session_id: str | None = None,
) -> str:
    pool = _pool(database)
    async with pool.acquire() as conn:
        exists = await conn.fetchval(
            "SELECT 1 FROM vsphere_library_items WHERE id = $1", library_item_id
        )
    if not exists:
        raise not_found(f"Library item {library_item_id} not found")
    resolved = session_id or f"download-session-{secrets.token_hex(6)}"
    files = {
        "descriptor.ovf": {
            "name": "descriptor.ovf",
            "size": 256,
            "status": "READY",
            "download_endpoint": {
                "uri": f"/api/content/library/item/download-session/{resolved}/file/descriptor.ovf",
            },
        }
    }
    await _upsert_session(
        database,
        session_id=resolved,
        kind="download",
        library_item_id=library_item_id,
        state="ACTIVE",
        files=files,
    )
    return resolved


async def get_download_session(database: Database, session_id: str) -> dict[str, Any]:
    session = await _get_session_row(database, session_id, "download")
    if session is None:
        raise not_found(f"Download session {session_id} not found")
    return {
        "id": session["id"],
        "library_item_id": session["library_item_id"],
        "state": session["state"],
    }


async def list_download_session_files(database: Database, session_id: str) -> list[dict[str, Any]]:
    session = await _get_session_row(database, session_id, "download")
    if session is None:
        raise not_found(f"Download session {session_id} not found")
    return [
        {
            "name": f["name"],
            "size": f["size"],
            "status": f["status"],
            "download_endpoint": f["download_endpoint"],
        }
        for f in session["files"].values()
    ]


async def deploy_ovf_from_library(
    database: Database,
    *,
    item_id: str,
    name: str,
    folder: str = "group-v23",
    host: str = "host-11",
    datastore: str = "datastore-31",
    resource_pool: str | None = None,
) -> tuple[str, str]:
    pool = _pool(database)
    async with pool.acquire() as conn:
        row = await conn.fetchrow("SELECT * FROM vsphere_library_items WHERE id = $1", item_id)
    if row is None:
        raise not_found(f"Library item {item_id} not found")
    moid = await inventory.next_moid(database, "vm")
    await inventory.upsert_object(
        database,
        moid=moid,
        type_name="VirtualMachine",
        name=name,
        parent_moid=folder,
        props={
            "power_state": "POWERED_OFF",
            "cpu_count": 2,
            "memory_size_mib": 2048,
            "guest_OS": "OTHER_GUEST_64",
            "hardware_version": "VMX_19",
            "host": host,
            "datastore": datastore,
            "resource_pool": resource_pool or "resgroup-22",
            "networks": ["network-41"],
            "identity": {"name": name},
            "deployed_from_library_item": item_id,
            "nics": [],
            "disks": [
                {
                    "key": "2000",
                    "value": {"label": "Hard disk 1", "capacity": 21474836480, "type": "SCSI"},
                }
            ],
        },
    )
    task_id = await task_store.create_task(
        database,
        description=f"Deploy OVF item {item_id} as {moid}",
        service="com.vmware.vcenter.ovf",
        operation="deploy",
        result={"vm": moid},
    )
    return moid, task_id


async def list_datastore_files(database: Database, datastore: str) -> list[dict[str, Any]]:
    obj = await inventory.get_object(database, datastore)
    if obj is None or obj.type != "Datastore":
        raise not_found(f"Datastore {datastore} not found")
    pool = _pool(database)
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            """
            SELECT path, size, type FROM vsphere_datastore_files
            WHERE datastore_moid = $1 ORDER BY path
            """,
            datastore,
        )
    return [{"path": row["path"], "size": row["size"], "type": row["type"]} for row in rows]


async def put_datastore_file(
    database: Database,
    datastore: str,
    path: str,
    *,
    size: int = 0,
    file_type: str = "FILE",
) -> None:
    obj = await inventory.get_object(database, datastore)
    if obj is None or obj.type != "Datastore":
        raise not_found(f"Datastore {datastore} not found")
    pool = _pool(database)
    async with pool.acquire() as conn:
        await conn.execute(
            """
            INSERT INTO vsphere_datastore_files (datastore_moid, path, size, type)
            VALUES ($1, $2, $3, $4)
            ON CONFLICT (datastore_moid, path) DO UPDATE SET size = EXCLUDED.size, type = EXCLUDED.type
            """,
            datastore,
            path,
            size,
            file_type,
        )


async def seed_platform_extras(database: Database, *, scale: int = 1) -> None:
    """Idempotent demo libraries/tags/files/sessions when platform tables exist.

    Always ensures stable lab IDs so probes and clients hit non-empty GETs:
    ``lib-local-1``, ``item-ubuntu``, ``cat-lab-1``, ``tag-lab-1``, ``session-lab-1``, …
    ``scale`` (1–4) adds proportional extra libraries/items/files for larger profiles.
    """

    from app.vsphere.domain import tasks as task_store

    scale = max(1, min(int(scale or 1), 8))

    # Ensure lab libraries/items even when older random-id rows already exist.
    lib = await create_library(
        database,
        name="Local Content",
        description="Lab library",
        library_id="lib-local-1",
    )
    await create_library_item(
        database,
        library_id=lib,
        name="ubuntu-22.04",
        item_type="ovf",
        description="Ubuntu template OVF",
        item_id="item-ubuntu",
    )
    await create_library_item(
        database,
        library_id=lib,
        name="centos-stream-9",
        item_type="ovf",
        description="CentOS Stream OVF",
        item_id="item-centos",
    )
    pub = await create_library(
        database,
        name="Published Templates",
        description="Published lab library",
        library_id="lib-published-1",
    )
    await create_library_item(
        database,
        library_id=pub,
        name="golden-image",
        item_type="ovf",
        description="Golden image OVF",
        item_id="item-golden",
    )

    # Proportional extras for large/big tiers.
    for index in range(2, scale + 1):
        extra_lib = await create_library(
            database,
            name=f"Team Library {index}",
            description=f"Scaled lab library {index}",
            library_id=f"lib-local-{index}",
        )
        await create_library_item(
            database,
            library_id=extra_lib,
            name=f"template-{index:02d}",
            item_type="ovf",
            description=f"Scaled template {index}",
            item_id=f"item-lab-{index}",
        )

    env = await tagging.create_category(
        database,
        name="Environment",
        description="Environment tags",
        associable_types=["VirtualMachine", "HostSystem"],
        category_id="urn:vmomi:InventoryServiceCategory:environment:GLOBAL",
    )
    owner = await tagging.create_category(
        database,
        name="Owner",
        description="Team ownership",
        associable_types=["VirtualMachine"],
        category_id="urn:vmomi:InventoryServiceCategory:owner:GLOBAL",
    )
    prod = await tagging.create_tag(
        database,
        category_id=env,
        name="prod",
        tag_id="urn:vmomi:InventoryServiceTag:prod:GLOBAL",
    )
    await tagging.create_tag(
        database,
        category_id=env,
        name="staging",
        tag_id="urn:vmomi:InventoryServiceTag:staging:GLOBAL",
    )
    await tagging.create_tag(
        database,
        category_id=owner,
        name="platform",
        tag_id="urn:vmomi:InventoryServiceTag:platform:GLOBAL",
    )
    await tagging.create_category(
        database,
        name="Lab",
        description="Probe alias category",
        associable_types=["VirtualMachine"],
        category_id="cat-lab-1",
    )
    await tagging.create_tag(
        database,
        category_id="cat-lab-1",
        name="lab",
        tag_id="tag-lab-1",
    )
    await tagging.attach_tag(database, prod, "VirtualMachine", "vm-101")
    await tagging.attach_tag(database, prod, "VirtualMachine", "vm-102")
    await tagging.attach_tag(database, "tag-lab-1", "VirtualMachine", "vm-101")

    # Proportional categories/tags for larger tiers (extras_scale).
    for index in range(2, scale + 1):
        cat_id = f"cat-scale-{index}"
        tag_id = f"tag-scale-{index}"
        await tagging.create_category(
            database,
            name=f"Scale {index}",
            description=f"Scaled category {index}",
            associable_types=["VirtualMachine", "HostSystem"],
            category_id=cat_id,
        )
        await tagging.create_tag(
            database,
            category_id=cat_id,
            name=f"tier-{index}",
            tag_id=tag_id,
        )
        await tagging.attach_tag(database, tag_id, "VirtualMachine", "vm-101")

    await put_datastore_file(
        database, "datastore-31", "[datastore1] ISO/ubuntu.iso", size=900000000
    )
    await put_datastore_file(
        database, "datastore-31", "[datastore1] ISO/vmware-tools.iso", size=120000000
    )
    await put_datastore_file(database, "datastore-31", "[datastore1] web-01/web-01.vmx", size=4096)
    await put_datastore_file(
        database, "datastore-31", "[datastore1] web-01/web-01.vmdk", size=42949672960
    )
    for index in range(2, scale + 1):
        ds = f"datastore-{30 + min(index, 8)}"
        await put_datastore_file(
            database,
            ds,
            f"[ds-{index:02d}] ISO/lab-media-{index}.iso",
            size=50_000_000 * index,
        )

    item_id = _LAB_ITEM_ID
    pool = _pool(database)
    async with pool.acquire() as conn:
        has_item = await conn.fetchval("SELECT 1 FROM vsphere_library_items WHERE id = $1", item_id)
    if has_item:
        await create_download_session(database, library_item_id=item_id, session_id=_LAB_SESSION_ID)
        await create_update_session(database, library_item_id=item_id, session_id=_LAB_SESSION_ID)

    # Lab snapshots so GET /snapshots is DB-backed (no create-on-read).
    from app.vsphere.domain import vm_ops

    for vm_moid in ("vm-101", "vm-102"):
        snaps = await vm_ops.list_snapshots(database, vm_moid)
        if not snaps:
            await vm_ops.create_snapshot(
                database, vm_moid, name="initial", description="Lab default snapshot"
            )

    await task_store.create_task(
        database,
        description="Lab inventory seed",
        service="com.vmware.vcenter",
        operation="seed",
        status="SUCCEEDED",
        result={"seeded": True, "profile": "lab"},
        task_id="task-1",
    )
