"""vSphere Automation REST API (/api + legacy /rest)."""

from __future__ import annotations

from typing import Any

from fastapi import APIRouter, Depends, Query, Request, Response
from fastapi.responses import JSONResponse
from fastapi.security import HTTPBasicCredentials

from app.db.pool import Database
from app.dependencies import get_database
from app.vsphere import inventory
from app.vsphere.domain import vm_ops
from app.vsphere.errors import invalid_argument, not_found, unauthenticated
from app.vsphere.rest import mappers
from app.vsphere.security.authz import require_privilege, require_read
from app.vsphere.security.session import (
    SESSION_HEADER,
    SessionInfo,
    create_session,
    delete_session,
    ensure_default_credentials,
    optional_basic,
    require_session,
    verify_password,
)

router = APIRouter(tags=["vSphere REST"])


def _session_json(session_id: str, *, legacy: bool = False) -> JSONResponse:
    # Modern /api/session returns a JSON string; legacy /rest returns {value: ...}.
    body: Any = {"value": session_id} if legacy else session_id
    return JSONResponse(content=body, status_code=201 if not legacy else 200)


def _attach_session_cookie(response: Response, session_id: str) -> None:
    response.headers[SESSION_HEADER] = session_id
    response.set_cookie(
        key=SESSION_HEADER,
        value=session_id,
        httponly=False,
        samesite="strict",
        path="/",
        max_age=(2 * 60 * 60),
    )


@router.post("/api/session")
async def create_api_session(
    request: Request,
    database: Database = Depends(get_database),
    credentials: HTTPBasicCredentials | None = Depends(optional_basic),
) -> Response:
    await ensure_default_credentials(database)
    username, password = await _credentials_from_request(request, credentials)
    if not await verify_password(database, username, password):
        raise unauthenticated("Invalid credentials")
    session_id = await create_session(database, username)
    response = _session_json(session_id)
    _attach_session_cookie(response, session_id)
    return response


@router.delete("/api/session")
async def delete_api_session(
    session: SessionInfo = Depends(require_session),
    database: Database = Depends(get_database),
) -> Response:
    await delete_session(database, session.id)
    response = Response(status_code=204)
    response.delete_cookie(SESSION_HEADER, path="/")
    return response


@router.get("/api/session")
async def get_api_session(session: SessionInfo = Depends(require_session)) -> Response:
    # vSphere Automation: validate session with HTTP 200 and empty body.
    # Lab helpers expose identity via optional headers for the web console.
    response = Response(status_code=200)
    response.headers["x-vmware-session-user"] = session.username
    response.headers["x-vmware-session-roles"] = ",".join(session.roles)
    return response


@router.post("/rest/com/vmware/cis/session")
async def create_legacy_session(
    request: Request,
    database: Database = Depends(get_database),
    credentials: HTTPBasicCredentials | None = Depends(optional_basic),
) -> Response:
    await ensure_default_credentials(database)
    username, password = await _credentials_from_request(request, credentials)
    if not await verify_password(database, username, password):
        raise unauthenticated("Invalid credentials")
    session_id = await create_session(database, username)
    response = _session_json(session_id, legacy=True)
    _attach_session_cookie(response, session_id)
    return response


@router.get("/rest/com/vmware/cis/session")
async def get_legacy_session(session: SessionInfo = Depends(require_session)) -> dict[str, str]:
    return {"value": session.id}


@router.delete("/rest/com/vmware/cis/session")
async def delete_legacy_session(
    session: SessionInfo = Depends(require_session),
    database: Database = Depends(get_database),
) -> dict[str, Any]:
    await delete_session(database, session.id)
    return {"value": None}


@router.get("/api/appliance/system/version")
async def appliance_version(
    request: Request,
    database: Database = Depends(get_database),
) -> dict[str, Any]:
    """Lab-friendly: version is readable without a session (real vCenter varies)."""

    from app.vsphere.domain import api_state

    payload = await api_state.get_payload(database, "GET", "/api/appliance/system/version")
    if isinstance(payload, dict) and payload:
        # Overlay active runtime major when contract apply changed it.
        major = getattr(request.app.state, "runtime_source_version", None)
        if major:
            out = dict(payload)
            out["version"] = str(major)
            return out
        return payload
    return {}


@router.get("/api/vcenter/vm")
async def list_vms(
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
    names: list[str] | None = Query(default=None),
    power_states: list[str] | None = Query(default=None),
    hosts: list[str] | None = Query(default=None),
    folders: list[str] | None = Query(default=None),
    datacenters: list[str] | None = Query(default=None),
    clusters: list[str] | None = Query(default=None),
    resource_pools: list[str] | None = Query(default=None),
    limit: int | None = Query(default=None, ge=1, le=5000),
    cursor: int = Query(default=0, ge=0),
) -> list[dict[str, Any]]:
    objects = await inventory.list_objects(database, type_name="VirtualMachine")
    result_objs = objects
    if names:
        wanted = set(names)
        result_objs = [obj for obj in result_objs if obj.name in wanted]
    if power_states:
        wanted_states = set(power_states)
        result_objs = [obj for obj in result_objs if obj.props.get("power_state") in wanted_states]
    if hosts:
        host_set = set(hosts)
        result_objs = [obj for obj in result_objs if obj.props.get("host") in host_set]
    if folders:
        folder_set = set(folders)
        result_objs = [obj for obj in result_objs if obj.parent_moid in folder_set]
    if resource_pools:
        rp_set = set(resource_pools)
        result_objs = [obj for obj in result_objs if obj.props.get("resource_pool") in rp_set]
    if clusters or datacenters:
        all_objects = {obj.moid: obj for obj in await inventory.list_objects(database)}
        if clusters:
            cluster_set = set(clusters)
            result_objs = [
                obj
                for obj in result_objs
                if all_objects.get(str(obj.props.get("host") or ""), None)
                and all_objects[str(obj.props.get("host"))].parent_moid in cluster_set
            ]
        if datacenters:
            dc_set = set(datacenters)
            filtered = []
            for obj in result_objs:
                parent = all_objects.get(obj.parent_moid or "")
                grand = (
                    all_objects.get(parent.parent_moid) if parent and parent.parent_moid else None
                )
                if (parent and parent.moid in dc_set) or (grand and grand.moid in dc_set):
                    filtered.append(obj)
            result_objs = filtered
    if limit is not None:
        result_objs = result_objs[cursor : cursor + limit]
    elif cursor:
        result_objs = result_objs[cursor:]
    return [mappers.vm_summary(obj) for obj in result_objs]


@router.get("/api/vcenter/vm/{vm}")
async def get_vm(
    vm: str,
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> dict[str, Any]:
    obj = await inventory.get_object(database, vm)
    if obj is None or obj.type != "VirtualMachine":
        raise not_found(f"VM {vm} not found")
    return mappers.vm_info(obj)


@router.post("/api/vcenter/vm")
async def create_vm(
    body: dict[str, Any],
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_privilege("VirtualMachine.Inventory.Create")),
) -> str:
    name = str(body.get("name") or "").strip()
    if not name:
        raise invalid_argument("name is required")
    guest = body.get("guest_OS") or body.get("guest_os") or "OTHER_GUEST_64"
    placement = body.get("placement") or {}
    folder = placement.get("folder") or "group-v23"
    host = placement.get("host") or "host-11"
    datastore = placement.get("datastore") or "datastore-31"
    pool = placement.get("resource_pool") or placement.get("cluster") or "resgroup-22"
    cpu = int((body.get("cpu") or {}).get("count") or body.get("cpu_count") or 1)
    memory = int((body.get("memory") or {}).get("size_MiB") or body.get("memory_size_MiB") or 1024)
    disks_spec = body.get("disks") or body.get("disk")
    nics_spec = body.get("nics") or body.get("ethernet")
    networks = body.get("networks") or ["network-41"]
    disks = None
    if isinstance(disks_spec, list) and disks_spec:
        disks = []
        for idx, disk in enumerate(disks_spec):
            value = disk.get("new_vmdk") or disk.get("value") or disk
            capacity = int(value.get("capacity") or value.get("capacity_bytes") or 42949672960)
            disks.append(
                {
                    "key": str(2000 + idx),
                    "value": {
                        "label": f"Hard disk {idx + 1}",
                        "capacity": capacity,
                        "type": "SCSI",
                    },
                }
            )
    nics = None
    if isinstance(nics_spec, list) and nics_spec:
        nics = []
        for idx, nic in enumerate(nics_spec):
            value = nic.get("value") or nic
            backing = value.get("backing") or {}
            network = str(backing.get("network") or (networks[0] if networks else "network-41"))
            nics.append(
                {
                    "key": str(4000 + idx),
                    "value": {
                        "label": f"Network adapter {idx + 1}",
                        "mac": value.get("mac_address") or f"00:50:56:01:00:{idx:02x}",
                        "state": "NOT_CONNECTED",
                        "type": value.get("type") or "VMXNET3",
                        "backing": {"type": "STANDARD_PORTGROUP", "network": network},
                    },
                }
            )
            if network not in networks:
                networks.append(network)
    moid, _task = await vm_ops.create_vm(
        database,
        name=name,
        folder=str(folder),
        host=str(host),
        datastore=str(datastore),
        resource_pool=str(pool),
        guest_os=str(guest),
        cpu_count=cpu,
        memory_size_mib=memory,
        networks=[str(n) for n in networks],
        disks=disks,
        nics=nics,
    )
    return moid


@router.delete("/api/vcenter/vm/{vm}")
async def delete_vm(
    vm: str,
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_privilege("VirtualMachine.Inventory.Delete")),
) -> Response:
    obj = await inventory.get_object(database, vm)
    if obj is None or obj.type != "VirtualMachine":
        raise not_found(f"VM {vm} not found")
    if obj.props.get("power_state") == "POWERED_ON":
        raise invalid_argument("VM must be powered off before delete")
    await inventory.delete_object(database, vm)
    return Response(status_code=204)


@router.get("/api/vcenter/vm/{vm}/power")
async def get_vm_power(
    vm: str,
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> dict[str, str]:
    obj = await inventory.get_object(database, vm)
    if obj is None or obj.type != "VirtualMachine":
        raise not_found(f"VM {vm} not found")
    state = str(obj.props.get("power_state") or "POWERED_OFF")
    return {"state": state}


@router.post("/api/vcenter/vm/{vm}/power")
async def power_vm(
    vm: str,
    action: str = Query(...),
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_privilege("VirtualMachine.Interact.PowerOn")),
) -> dict[str, str]:
    task_id = await vm_ops.set_power(database, vm, action.lower())
    return {"task": task_id}


@router.get("/api/vcenter/host")
async def list_hosts(
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> list[dict[str, Any]]:
    objects = await inventory.list_objects(database, type_name="HostSystem")
    return [mappers.host_summary(obj) for obj in objects]


@router.get("/api/vcenter/host/{host}")
async def get_host(
    host: str,
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> dict[str, Any]:
    obj = await inventory.get_object(database, host)
    if obj is None or obj.type != "HostSystem":
        raise not_found(f"Host {host} not found")
    return mappers.host_info(obj)


@router.get("/api/vcenter/datastore")
async def list_datastores(
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> list[dict[str, Any]]:
    objects = await inventory.list_objects(database, type_name="Datastore")
    return [mappers.datastore_summary(obj) for obj in objects]


@router.get("/api/vcenter/datastore/{datastore}")
async def get_datastore(
    datastore: str,
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> 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")
    props = obj.props
    return {
        "name": obj.name,
        "type": props.get("type", "VMFS"),
        "accessible": props.get("accessible", True),
        "free_space": props.get("free_space", 0),
        "capacity": props.get("capacity", 0),
        "multiple_host_access": props.get("multiple_host_access", True),
    }


@router.get("/api/vcenter/network")
async def list_networks(
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> list[dict[str, Any]]:
    networks = await inventory.list_objects(database, type_name="Network")
    dvpg = await inventory.list_objects(database, type_name="DistributedVirtualPortgroup")
    return [mappers.network_summary(obj) for obj in (*networks, *dvpg)]


@router.get("/api/vcenter/datacenter")
async def list_datacenters(
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> list[dict[str, Any]]:
    objects = await inventory.list_objects(database, type_name="Datacenter")
    return [mappers.datacenter_summary(obj) for obj in objects]


@router.get("/api/vcenter/cluster")
async def list_clusters(
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> list[dict[str, Any]]:
    objects = await inventory.list_objects(database, type_name="ClusterComputeResource")
    return [mappers.cluster_summary(obj) for obj in objects]


@router.get("/api/vcenter/folder")
async def list_folders(
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> list[dict[str, Any]]:
    objects = await inventory.list_objects(database, type_name="Folder")
    return [mappers.folder_summary(obj) for obj in objects]


@router.get("/api/vcenter/resource-pool")
async def list_resource_pools(
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> list[dict[str, Any]]:
    objects = await inventory.list_objects(database, type_name="ResourcePool")
    return [mappers.resource_pool_summary(obj) for obj in objects]


@router.get("/api/vcenter/vm/{vm}/guest/identity")
async def vm_guest_identity(
    vm: str,
    database: Database = Depends(get_database),
    _: SessionInfo = Depends(require_read),
) -> dict[str, Any]:
    obj = await inventory.get_object(database, vm)
    if obj is None or obj.type != "VirtualMachine":
        raise not_found(f"VM {vm} not found")
    return {
        "name": obj.name,
        "family": "LINUX",
        "full_name": {"default_message": obj.props.get("guest_OS", "OTHER")},
        "ip_address": "10.0.0." + obj.moid.split("-")[-1],
        "host_name": obj.name,
    }


async def _credentials_from_request(
    request: Request,
    credentials: HTTPBasicCredentials | None,
) -> tuple[str, str]:
    if credentials is not None:
        return credentials.username, credentials.password
    # Some clients post JSON credentials (lab convenience).
    if request.headers.get("content-type", "").startswith("application/json"):
        try:
            payload = await request.json()
        except Exception:
            payload = {}
        if isinstance(payload, dict) and payload.get("user_name") and payload.get("password"):
            return str(payload["user_name"]), str(payload["password"])
    raise unauthenticated("Basic authentication required")
