"""Playbook endpoints for triggering infrastructure automation."""

import re
import uuid

import httpx
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select

from app.db import AsyncSessionDep
from app.models.agent import Agent
from app.models.task import Task
from app.models.tenant import Tenant
from app.playbooks.chatwoot import (
    create_chatwoot_initial_setup_task,
    create_chatwoot_setup_task,
)
from app.playbooks.nextcloud import (
    create_nextcloud_initial_setup_task,
    create_nextcloud_set_domain_task,
)
from app.playbooks.poste import create_poste_initial_setup_task
from app.playbooks.keycloak import (
    create_keycloak_initial_setup_task,
    create_keycloak_setup_task,
)
from app.playbooks.n8n import (
    create_n8n_initial_setup_task,
    create_n8n_setup_task,
)
from app.playbooks.calcom import (
    create_calcom_initial_setup_task,
    create_calcom_setup_task,
)
from app.playbooks.umami import (
    create_umami_initial_setup_task,
    create_umami_setup_task,
)
from app.playbooks.uptime_kuma import (
    create_uptime_kuma_initial_setup_task,
    create_uptime_kuma_setup_task,
)
from app.playbooks.vaultwarden import create_vaultwarden_setup_task
from app.playbooks.portainer import create_portainer_setup_task
from app.schemas.task import TaskResponse

router = APIRouter(prefix="/tenants/{tenant_id}", tags=["Playbooks"])

# Health check timeout for Nextcloud availability
HEALTH_CHECK_TIMEOUT = 10.0


class ChatwootSetupRequest(BaseModel):
    """Request body for Chatwoot setup playbook."""

    agent_id: uuid.UUID | None = Field(
        None, description="Optional agent UUID to assign the task to"
    )
    domain: str = Field(
        ..., min_length=1, description="Domain for Chatwoot (e.g., support.example.com)"
    )


class NextcloudSetDomainRequest(BaseModel):
    """Request body for Nextcloud set-domain playbook."""

    public_url: str = Field(
        ...,
        min_length=1,
        description="Public URL for Nextcloud (e.g., https://cloud.example.com)",
    )
    pull: bool = Field(
        False,
        description="Whether to pull images before reloading the stack",
    )


class NextcloudInitialSetupRequest(BaseModel):
    """Request body for Nextcloud initial setup via Playwright."""

    admin_username: str = Field(
        ...,
        min_length=3,
        max_length=64,
        description="Username for the Nextcloud admin account",
    )
    admin_password: str = Field(
        ...,
        min_length=8,
        description="Password for the Nextcloud admin account (minimum 8 characters)",
    )

    @field_validator("admin_username")
    @classmethod
    def validate_username(cls, v: str) -> str:
        """Validate that username is alphanumeric or a valid email address."""
        username_pattern = r"^[a-zA-Z][a-zA-Z0-9_]*$"
        email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        if not (re.match(username_pattern, v) or re.match(email_pattern, v)):
            raise ValueError(
                "Username must be alphanumeric (starting with a letter) or a valid email address"
            )
        return v




class PosteInitialSetupRequest(BaseModel):
    """Request body for Poste.io initial setup via Playwright."""

    admin_email: str = Field(
        ...,
        min_length=5,
        description="Email address for the Poste.io admin account (e.g., admin@example.com)",
    )
    admin_password: str | None = Field(
        None,
        min_length=8,
        description="Password for admin account (auto-generated if not provided)",
    )

    @field_validator("admin_email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        """Validate that admin_email is a valid email address."""
        email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        if not re.match(email_pattern, v):
            raise ValueError("admin_email must be a valid email address")
        return v


class ChatwootInitialSetupRequest(BaseModel):
    """Request body for Chatwoot initial setup via Playwright."""

    admin_name: str = Field(
        ...,
        min_length=2,
        max_length=100,
        description="Full name for the Chatwoot admin account",
    )
    company_name: str = Field(
        ...,
        min_length=2,
        max_length=100,
        description="Company/organization name",
    )
    admin_email: str = Field(
        ...,
        min_length=5,
        description="Email address for the Chatwoot admin account",
    )
    admin_password: str | None = Field(
        None,
        min_length=8,
        description="Password for admin account (auto-generated if not provided)",
    )

    @field_validator("admin_email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        """Validate that admin_email is a valid email address."""
        email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        if not re.match(email_pattern, v):
            raise ValueError("admin_email must be a valid email address")
        return v


class KeycloakSetupRequest(BaseModel):
    """Request body for Keycloak setup playbook."""

    agent_id: uuid.UUID | None = Field(
        None, description="Optional agent UUID to assign the task to"
    )
    domain: str = Field(
        ..., min_length=1, description="Domain for Keycloak (e.g., auth.example.com)"
    )
    admin_user: str = Field(
        "admin", min_length=1, description="Admin username (default: admin)"
    )
    admin_password: str = Field(
        ..., min_length=8, description="Admin password (minimum 8 characters)"
    )


class KeycloakInitialSetupRequest(BaseModel):
    """Request body for Keycloak initial setup via Playwright."""

    admin_user: str = Field(
        "admin", min_length=1, description="Admin username"
    )
    admin_password: str = Field(
        ..., min_length=8, description="Admin password"
    )
    realm_name: str = Field(
        "letsbe", min_length=1, max_length=64, description="Realm name to create"
    )


class N8nSetupRequest(BaseModel):
    """Request body for n8n setup playbook."""

    agent_id: uuid.UUID | None = Field(
        None, description="Optional agent UUID to assign the task to"
    )
    domain: str = Field(
        ..., min_length=1, description="Domain for n8n (e.g., n8n.example.com)"
    )


class N8nInitialSetupRequest(BaseModel):
    """Request body for n8n initial setup via Playwright."""

    admin_email: str = Field(
        ..., min_length=5, description="Email address for the n8n owner account"
    )
    admin_password: str | None = Field(
        None, min_length=8, description="Password for owner (auto-generated if not provided)"
    )
    admin_first_name: str = Field(
        "Admin", min_length=1, description="First name for the owner account"
    )
    admin_last_name: str = Field(
        "User", min_length=1, description="Last name for the owner account"
    )

    @field_validator("admin_email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        if not re.match(email_pattern, v):
            raise ValueError("admin_email must be a valid email address")
        return v


class CalcomSetupRequest(BaseModel):
    """Request body for Cal.com setup playbook."""

    agent_id: uuid.UUID | None = Field(
        None, description="Optional agent UUID to assign the task to"
    )
    domain: str = Field(
        ..., min_length=1, description="Domain for Cal.com (e.g., cal.example.com)"
    )


class CalcomInitialSetupRequest(BaseModel):
    """Request body for Cal.com initial setup via Playwright."""

    admin_email: str = Field(
        ..., min_length=5, description="Email address for the admin account"
    )
    admin_password: str | None = Field(
        None, min_length=8, description="Password for admin (auto-generated if not provided)"
    )
    admin_username: str = Field(
        "admin", min_length=1, max_length=64, description="Username for the admin account"
    )
    admin_name: str = Field(
        "Admin", min_length=1, max_length=100, description="Display name for the admin account"
    )

    @field_validator("admin_email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        if not re.match(email_pattern, v):
            raise ValueError("admin_email must be a valid email address")
        return v


class UmamiSetupRequest(BaseModel):
    """Request body for Umami setup playbook."""

    agent_id: uuid.UUID | None = Field(
        None, description="Optional agent UUID to assign the task to"
    )
    domain: str = Field(
        ..., min_length=1, description="Domain for Umami (e.g., analytics.example.com)"
    )


class UmamiInitialSetupRequest(BaseModel):
    """Request body for Umami initial setup via Playwright."""

    admin_password: str | None = Field(
        None, min_length=8, description="New admin password (auto-generated if not provided)"
    )
    website_name: str | None = Field(
        None, description="Optional name of the first website to add"
    )
    website_url: str | None = Field(
        None, description="Optional URL of the first website to track"
    )


class UptimeKumaSetupRequest(BaseModel):
    """Request body for Uptime Kuma setup playbook."""

    agent_id: uuid.UUID | None = Field(
        None, description="Optional agent UUID to assign the task to"
    )
    domain: str = Field(
        ..., min_length=1, description="Domain for Uptime Kuma (e.g., status.example.com)"
    )


class UptimeKumaInitialSetupRequest(BaseModel):
    """Request body for Uptime Kuma initial setup via Playwright."""

    admin_username: str = Field(
        "admin", min_length=1, max_length=64, description="Username for the admin account"
    )
    admin_password: str | None = Field(
        None, min_length=8, description="Password for admin (auto-generated if not provided)"
    )


class VaultwardenSetupRequest(BaseModel):
    """Request body for Vaultwarden setup playbook."""

    agent_id: uuid.UUID | None = Field(
        None, description="Optional agent UUID to assign the task to"
    )
    domain: str = Field(
        ..., min_length=1, description="Domain for Vaultwarden (e.g., vault.example.com)"
    )
    admin_token: str = Field(
        ..., min_length=8, description="Admin panel access token"
    )
    signups_allowed: bool = Field(
        True, description="Whether new user registration is allowed"
    )


class PortainerSetupRequest(BaseModel):
    """Request body for Portainer setup playbook."""

    agent_id: uuid.UUID | None = Field(
        None, description="Optional agent UUID to assign the task to"
    )
    domain: str = Field(
        ..., min_length=1, description="Domain for Portainer (e.g., portainer.example.com)"
    )


# --- Helper functions ---


async def get_tenant_by_id(db: AsyncSessionDep, tenant_id: uuid.UUID) -> Tenant | None:
    """Retrieve a tenant by ID."""
    result = await db.execute(select(Tenant).where(Tenant.id == tenant_id))
    return result.scalar_one_or_none()


async def get_agent_by_id(db: AsyncSessionDep, agent_id: uuid.UUID) -> Agent | None:
    """Retrieve an agent by ID."""
    result = await db.execute(select(Agent).where(Agent.id == agent_id))
    return result.scalar_one_or_none()


async def get_online_agent_for_tenant(
    db: AsyncSessionDep, tenant_id: uuid.UUID
) -> Agent | None:
    """Get the first online agent for a tenant."""
    result = await db.execute(
        select(Agent)
        .where(Agent.tenant_id == tenant_id)
        .where(Agent.status == "online")
        .limit(1)
    )
    return result.scalar_one_or_none()


async def check_nextcloud_availability(base_url: str) -> bool:
    """
    Check if Nextcloud is available at the given URL.

    Performs a health check against {base_url}/status.php which returns
    JSON with installation status on a healthy Nextcloud instance.

    Args:
        base_url: The base URL for Nextcloud (e.g., "https://cloud.example.com")

    Returns:
        True if Nextcloud responds with a valid status, False otherwise
    """
    status_url = f"{base_url.rstrip('/')}/status.php"

    try:
        async with httpx.AsyncClient(
            timeout=HEALTH_CHECK_TIMEOUT,
            follow_redirects=True,
        ) as client:
            response = await client.get(status_url)

            # Nextcloud status.php returns 200 with JSON when healthy
            if response.status_code == 200:
                # Try to parse as JSON - valid Nextcloud returns {"installed":true,...}
                try:
                    data = response.json()
                    # If we get valid JSON with "installed" key, Nextcloud is available
                    return "installed" in data
                except (ValueError, KeyError):
                    # Not valid JSON, but 200 OK might still indicate availability
                    return True

            return False

    except (httpx.RequestError, httpx.TimeoutException):
        # Connection failed, timeout, or other network error
        return False




async def check_poste_availability(base_url: str) -> bool:
    """
    Check if Poste.io is available at the given URL.

    Performs a health check against the base URL to verify the service is up.

    Args:
        base_url: The base URL for Poste.io (e.g., "https://mail.example.com")

    Returns:
        True if Poste.io responds, False otherwise
    """
    try:
        async with httpx.AsyncClient(
            timeout=HEALTH_CHECK_TIMEOUT,
            follow_redirects=True,
        ) as client:
            response = await client.get(base_url.rstrip("/"))
            # Poste.io returns 200 on setup page or login page
            return response.status_code == 200

    except (httpx.RequestError, httpx.TimeoutException):
        return False


async def check_chatwoot_availability(base_url: str) -> bool:
    """
    Check if Chatwoot is available at the given URL.

    Performs a health check against the base URL to verify the service is up.

    Args:
        base_url: The base URL for Chatwoot (e.g., "https://chatwoot.example.com")

    Returns:
        True if Chatwoot responds, False otherwise
    """
    try:
        async with httpx.AsyncClient(
            timeout=HEALTH_CHECK_TIMEOUT,
            follow_redirects=True,
        ) as client:
            response = await client.get(base_url.rstrip("/"))
            # Chatwoot returns 200 on setup page or login page
            return response.status_code == 200

    except (httpx.RequestError, httpx.TimeoutException):
        return False


async def check_service_availability(base_url: str) -> bool:
    """
    Generic health check for a web service.

    Performs a simple GET request to verify the service is up and responding.

    Args:
        base_url: The base URL for the service

    Returns:
        True if the service responds with HTTP 200, False otherwise
    """
    try:
        async with httpx.AsyncClient(
            timeout=HEALTH_CHECK_TIMEOUT,
            follow_redirects=True,
        ) as client:
            response = await client.get(base_url.rstrip("/"))
            return response.status_code == 200

    except (httpx.RequestError, httpx.TimeoutException):
        return False


# --- Route handlers ---


@router.post(
    "/chatwoot/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def setup_chatwoot(
    tenant_id: uuid.UUID,
    request: ChatwootSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Trigger Chatwoot setup playbook for a tenant.

    Creates a COMPOSITE task with the following steps:
    1. **ENV_UPDATE**: Set FRONTEND_URL and BACKEND_URL in chatwoot.env
    2. **DOCKER_RELOAD**: Restart the Chatwoot stack with pull=True

    ## Request Body
    - **agent_id**: Optional agent UUID to assign the task to immediately
    - **domain**: The domain for Chatwoot (e.g., "support.example.com")

    ## Response
    Returns the created Task with type="COMPOSITE" and status="pending".

    The SysAdmin Agent will pick up this task and execute the steps in sequence.
    """
    # Validate tenant exists
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    # Validate agent exists if provided
    if request.agent_id is not None:
        agent = await get_agent_by_id(db, request.agent_id)
        if agent is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Agent {request.agent_id} not found",
            )

    # Create the COMPOSITE task
    task = await create_chatwoot_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=request.agent_id,
        domain=request.domain,
    )

    return task


@router.post(
    "/nextcloud/set-domain",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def nextcloud_set_domain(
    tenant_id: uuid.UUID,
    request: NextcloudSetDomainRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Set Nextcloud domain for a tenant (v1 - reload only).

    Creates a COMPOSITE task with a single DOCKER_RELOAD step.
    Auto-resolves the agent from the tenant (first online agent).

    **Note:** This is v1 of the playbook. It only reloads the Nextcloud stack.
    Future versions will add occ commands to configure config.php.

    ## Request Body
    - **public_url**: The public URL for Nextcloud (e.g., "https://cloud.example.com")
    - **pull**: Whether to pull images before reloading (default: false)

    ## Response
    Returns the created Task with type="COMPOSITE" and status="pending".

    The SysAdmin Agent will pick up this task and execute the steps in sequence.
    """
    # Validate tenant exists
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    # Auto-resolve agent (first online agent for tenant)
    agent = await get_online_agent_for_tenant(db, tenant_id)
    if agent is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No online agent found for tenant {tenant_id}",
        )

    # Create the COMPOSITE task
    task = await create_nextcloud_set_domain_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=agent.id,
        public_url=request.public_url,
        pull=request.pull,
    )

    return task


@router.post(
    "/nextcloud/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def nextcloud_initial_setup(
    tenant_id: uuid.UUID,
    request: NextcloudInitialSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Perform initial Nextcloud setup via Playwright browser automation.

    Creates a PLAYWRIGHT task that navigates to the Nextcloud installation page
    and creates the admin account. This should only be called once on a fresh
    Nextcloud installation.

    **Prerequisites:**
    - Tenant must have a domain configured
    - Nextcloud must be deployed and accessible at `https://cloud.{domain}`
    - An online agent must be registered for the tenant

    **Health Check:**
    Before creating the task, the endpoint checks if Nextcloud is available
    at `https://cloud.{domain}/status.php`. If not reachable, returns 409.

    ## Request Body
    - **admin_username**: Username for the admin account (3-64 chars, alphanumeric)
    - **admin_password**: Password for the admin account (minimum 8 characters)

    ## Response
    Returns the created Task with type="PLAYWRIGHT" and status="pending".

    ## Errors
    - **404**: Tenant not found
    - **400**: Tenant has no domain configured
    - **404**: No online agent found for tenant
    - **409**: Nextcloud not available (health check failed)
    - **422**: Invalid username or password format
    """
    # Validate tenant exists
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    # Validate tenant has domain configured
    if not tenant.domain:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Tenant {tenant_id} has no domain configured",
        )

    # Auto-resolve agent (first online agent for tenant)
    agent = await get_online_agent_for_tenant(db, tenant_id)
    if agent is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No online agent found for tenant {tenant_id}",
        )

    # Construct Nextcloud base URL from tenant domain
    base_url = f"https://cloud.{tenant.domain}"

    # Health check: verify Nextcloud is accessible
    is_available = await check_nextcloud_availability(base_url)
    if not is_available:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Nextcloud not available at {base_url}. "
            "Ensure Nextcloud is deployed and accessible before running setup.",
        )

    # Create the PLAYWRIGHT task
    task = await create_nextcloud_initial_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=agent.id,
        base_url=base_url,
        admin_username=request.admin_username,
        admin_password=request.admin_password,
    )

    return task


@router.post(
    "/poste/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def poste_initial_setup(
    tenant_id: uuid.UUID,
    request: PosteInitialSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Perform initial Poste.io setup via Playwright browser automation.

    Creates a PLAYWRIGHT task that navigates to the Poste.io setup wizard,
    configures the mailserver hostname, and creates the admin account.
    This should only be called once on a fresh Poste.io installation.

    **Prerequisites:**
    - Tenant must have a domain configured
    - Poste.io must be deployed and accessible at `https://mail.{domain}`
    - An online agent must be registered for the tenant

    **Health Check:**
    Before creating the task, the endpoint checks if Poste.io is available.
    If not reachable, returns 409.

    ## Request Body
    - **admin_email**: Email address for the admin account (e.g., admin@example.com)
    - **admin_password**: Password for admin (auto-generated if not provided)

    ## Response
    Returns the created Task with type="PLAYWRIGHT" and status="pending".

    The task result will contain the generated/provided credentials.

    ## Errors
    - **404**: Tenant not found
    - **400**: Tenant has no domain configured
    - **404**: No online agent found for tenant
    - **409**: Poste.io not available (health check failed)
    - **422**: Invalid email format
    """
    # Validate tenant exists
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    # Validate tenant has domain configured
    if not tenant.domain:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Tenant {tenant_id} has no domain configured",
        )

    # Auto-resolve agent (first online agent for tenant)
    agent = await get_online_agent_for_tenant(db, tenant_id)
    if agent is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No online agent found for tenant {tenant_id}",
        )

    # Construct Poste.io base URL from tenant domain
    base_url = f"https://mail.{tenant.domain}"

    # Health check: verify Poste.io is accessible
    is_available = await check_poste_availability(base_url)
    if not is_available:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Poste.io not available at {base_url}. "
            "Ensure Poste.io is deployed and accessible before running setup.",
        )

    # Create the PLAYWRIGHT task
    task = await create_poste_initial_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=agent.id,
        base_url=base_url,
        admin_email=request.admin_email,
        admin_password=request.admin_password,
    )

    return task


@router.post(
    "/chatwoot/initial-setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def chatwoot_initial_setup(
    tenant_id: uuid.UUID,
    request: ChatwootInitialSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Perform initial Chatwoot setup via Playwright browser automation.

    Creates a PLAYWRIGHT task that navigates to the Chatwoot setup wizard,
    creates the super admin account, unchecks newsletter subscription,
    and completes the setup. This should only be called once on a fresh
    Chatwoot installation.

    **Prerequisites:**
    - Tenant must have a domain configured
    - Chatwoot must be deployed and accessible at `https://chatwoot.{domain}`
    - An online agent must be registered for the tenant

    **Health Check:**
    Before creating the task, the endpoint checks if Chatwoot is available.
    If not reachable, returns 409.

    ## Request Body
    - **admin_name**: Full name for the admin account
    - **company_name**: Company/organization name
    - **admin_email**: Email address for the admin account
    - **admin_password**: Password for admin (auto-generated if not provided)

    ## Response
    Returns the created Task with type="PLAYWRIGHT" and status="pending".

    The task result will contain the generated/provided credentials.

    ## Errors
    - **404**: Tenant not found
    - **400**: Tenant has no domain configured
    - **404**: No online agent found for tenant
    - **409**: Chatwoot not available (health check failed)
    - **422**: Invalid email format
    """
    # Validate tenant exists
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    # Validate tenant has domain configured
    if not tenant.domain:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Tenant {tenant_id} has no domain configured",
        )

    # Auto-resolve agent (first online agent for tenant)
    agent = await get_online_agent_for_tenant(db, tenant_id)
    if agent is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No online agent found for tenant {tenant_id}",
        )

    # Construct Chatwoot base URL from tenant domain
    base_url = f"https://chatwoot.{tenant.domain}"

    # Health check: verify Chatwoot is accessible
    is_available = await check_chatwoot_availability(base_url)
    if not is_available:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Chatwoot not available at {base_url}. "
            "Ensure Chatwoot is deployed and accessible before running setup.",
        )

    # Create the PLAYWRIGHT task
    task = await create_chatwoot_initial_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=agent.id,
        base_url=base_url,
        admin_name=request.admin_name,
        company_name=request.company_name,
        admin_email=request.admin_email,
        admin_password=request.admin_password,
    )

    return task


# =============================================================================
# Keycloak
# =============================================================================


@router.post(
    "/keycloak/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def setup_keycloak(
    tenant_id: uuid.UUID,
    request: KeycloakSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Trigger Keycloak setup playbook for a tenant.

    Creates a COMPOSITE task with the following steps:
    1. **ENV_UPDATE**: Set KC_HOSTNAME, KEYCLOAK_ADMIN, KEYCLOAK_ADMIN_PASSWORD
    2. **DOCKER_RELOAD**: Restart the Keycloak stack with pull=True
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if request.agent_id is not None:
        agent = await get_agent_by_id(db, request.agent_id)
        if agent is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Agent {request.agent_id} not found",
            )

    task = await create_keycloak_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=request.agent_id,
        domain=request.domain,
        admin_user=request.admin_user,
        admin_password=request.admin_password,
    )

    return task


@router.post(
    "/keycloak/initial-setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def keycloak_initial_setup(
    tenant_id: uuid.UUID,
    request: KeycloakInitialSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Perform initial Keycloak setup via Playwright browser automation.

    Creates a PLAYWRIGHT task that logs into the Keycloak admin console
    and creates the "letsbe" realm. This should only be called once
    after Keycloak is deployed.

    **Prerequisites:**
    - Tenant must have a domain configured
    - Keycloak must be deployed and accessible at `https://auth.{domain}`
    - An online agent must be registered for the tenant
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if not tenant.domain:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Tenant {tenant_id} has no domain configured",
        )

    agent = await get_online_agent_for_tenant(db, tenant_id)
    if agent is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No online agent found for tenant {tenant_id}",
        )

    base_url = f"https://auth.{tenant.domain}"

    is_available = await check_service_availability(base_url)
    if not is_available:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Keycloak not available at {base_url}. "
            "Ensure Keycloak is deployed and accessible before running setup.",
        )

    task = await create_keycloak_initial_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=agent.id,
        base_url=base_url,
        admin_user=request.admin_user,
        admin_password=request.admin_password,
        realm_name=request.realm_name,
    )

    return task


# =============================================================================
# n8n
# =============================================================================


@router.post(
    "/n8n/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def setup_n8n(
    tenant_id: uuid.UUID,
    request: N8nSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Trigger n8n setup playbook for a tenant.

    Creates a COMPOSITE task with the following steps:
    1. **ENV_UPDATE**: Set N8N_HOST, N8N_PROTOCOL, WEBHOOK_URL
    2. **DOCKER_RELOAD**: Restart the n8n stack with pull=True
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if request.agent_id is not None:
        agent = await get_agent_by_id(db, request.agent_id)
        if agent is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Agent {request.agent_id} not found",
            )

    task = await create_n8n_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=request.agent_id,
        domain=request.domain,
    )

    return task


@router.post(
    "/n8n/initial-setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def n8n_initial_setup(
    tenant_id: uuid.UUID,
    request: N8nInitialSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Perform initial n8n setup via Playwright browser automation.

    Creates a PLAYWRIGHT task that creates the owner account on a fresh
    n8n installation.

    **Prerequisites:**
    - Tenant must have a domain configured
    - n8n must be deployed and accessible at `https://n8n.{domain}`
    - An online agent must be registered for the tenant
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if not tenant.domain:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Tenant {tenant_id} has no domain configured",
        )

    agent = await get_online_agent_for_tenant(db, tenant_id)
    if agent is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No online agent found for tenant {tenant_id}",
        )

    base_url = f"https://n8n.{tenant.domain}"

    is_available = await check_service_availability(base_url)
    if not is_available:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"n8n not available at {base_url}. "
            "Ensure n8n is deployed and accessible before running setup.",
        )

    task = await create_n8n_initial_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=agent.id,
        base_url=base_url,
        admin_email=request.admin_email,
        admin_password=request.admin_password,
        admin_first_name=request.admin_first_name,
        admin_last_name=request.admin_last_name,
    )

    return task


# =============================================================================
# Cal.com
# =============================================================================


@router.post(
    "/calcom/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def setup_calcom(
    tenant_id: uuid.UUID,
    request: CalcomSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Trigger Cal.com setup playbook for a tenant.

    Creates a COMPOSITE task with the following steps:
    1. **ENV_UPDATE**: Set NEXT_PUBLIC_WEBAPP_URL, NEXTAUTH_URL
    2. **DOCKER_RELOAD**: Restart the Cal.com stack with pull=True
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if request.agent_id is not None:
        agent = await get_agent_by_id(db, request.agent_id)
        if agent is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Agent {request.agent_id} not found",
            )

    task = await create_calcom_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=request.agent_id,
        domain=request.domain,
    )

    return task


@router.post(
    "/calcom/initial-setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def calcom_initial_setup(
    tenant_id: uuid.UUID,
    request: CalcomInitialSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Perform initial Cal.com setup via Playwright browser automation.

    Creates a PLAYWRIGHT task that creates the admin account on a fresh
    Cal.com installation.

    **Prerequisites:**
    - Tenant must have a domain configured
    - Cal.com must be deployed and accessible at `https://cal.{domain}`
    - An online agent must be registered for the tenant
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if not tenant.domain:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Tenant {tenant_id} has no domain configured",
        )

    agent = await get_online_agent_for_tenant(db, tenant_id)
    if agent is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No online agent found for tenant {tenant_id}",
        )

    base_url = f"https://cal.{tenant.domain}"

    is_available = await check_service_availability(base_url)
    if not is_available:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Cal.com not available at {base_url}. "
            "Ensure Cal.com is deployed and accessible before running setup.",
        )

    task = await create_calcom_initial_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=agent.id,
        base_url=base_url,
        admin_email=request.admin_email,
        admin_password=request.admin_password,
        admin_username=request.admin_username,
        admin_name=request.admin_name,
    )

    return task


# =============================================================================
# Umami
# =============================================================================


@router.post(
    "/umami/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def setup_umami(
    tenant_id: uuid.UUID,
    request: UmamiSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Trigger Umami setup playbook for a tenant.

    Creates a COMPOSITE task with the following steps:
    1. **ENV_UPDATE**: Set APP_URL
    2. **DOCKER_RELOAD**: Restart the Umami stack with pull=True
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if request.agent_id is not None:
        agent = await get_agent_by_id(db, request.agent_id)
        if agent is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Agent {request.agent_id} not found",
            )

    task = await create_umami_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=request.agent_id,
        domain=request.domain,
    )

    return task


@router.post(
    "/umami/initial-setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def umami_initial_setup(
    tenant_id: uuid.UUID,
    request: UmamiInitialSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Perform initial Umami setup via Playwright browser automation.

    Creates a PLAYWRIGHT task that logs in with default credentials
    (admin/umami), changes the admin password, and optionally adds
    the first website to track.

    **Prerequisites:**
    - Tenant must have a domain configured
    - Umami must be deployed and accessible at `https://analytics.{domain}`
    - An online agent must be registered for the tenant
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if not tenant.domain:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Tenant {tenant_id} has no domain configured",
        )

    agent = await get_online_agent_for_tenant(db, tenant_id)
    if agent is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No online agent found for tenant {tenant_id}",
        )

    base_url = f"https://analytics.{tenant.domain}"

    is_available = await check_service_availability(base_url)
    if not is_available:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Umami not available at {base_url}. "
            "Ensure Umami is deployed and accessible before running setup.",
        )

    task = await create_umami_initial_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=agent.id,
        base_url=base_url,
        admin_password=request.admin_password,
        website_name=request.website_name,
        website_url=request.website_url,
    )

    return task


# =============================================================================
# Uptime Kuma
# =============================================================================


@router.post(
    "/uptime-kuma/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def setup_uptime_kuma(
    tenant_id: uuid.UUID,
    request: UptimeKumaSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Trigger Uptime Kuma setup playbook for a tenant.

    Creates a COMPOSITE task with the following steps:
    1. **ENV_UPDATE**: Set UPTIME_KUMA_DOMAIN
    2. **DOCKER_RELOAD**: Restart the Uptime Kuma stack with pull=True
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if request.agent_id is not None:
        agent = await get_agent_by_id(db, request.agent_id)
        if agent is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Agent {request.agent_id} not found",
            )

    task = await create_uptime_kuma_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=request.agent_id,
        domain=request.domain,
    )

    return task


@router.post(
    "/uptime-kuma/initial-setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def uptime_kuma_initial_setup(
    tenant_id: uuid.UUID,
    request: UptimeKumaInitialSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Perform initial Uptime Kuma setup via Playwright browser automation.

    Creates a PLAYWRIGHT task that creates the admin account on a fresh
    Uptime Kuma installation.

    **Prerequisites:**
    - Tenant must have a domain configured
    - Uptime Kuma must be deployed and accessible at `https://status.{domain}`
    - An online agent must be registered for the tenant
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if not tenant.domain:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Tenant {tenant_id} has no domain configured",
        )

    agent = await get_online_agent_for_tenant(db, tenant_id)
    if agent is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No online agent found for tenant {tenant_id}",
        )

    base_url = f"https://status.{tenant.domain}"

    is_available = await check_service_availability(base_url)
    if not is_available:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Uptime Kuma not available at {base_url}. "
            "Ensure Uptime Kuma is deployed and accessible before running setup.",
        )

    task = await create_uptime_kuma_initial_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=agent.id,
        base_url=base_url,
        admin_username=request.admin_username,
        admin_password=request.admin_password,
    )

    return task


# =============================================================================
# Vaultwarden (no Playwright)
# =============================================================================


@router.post(
    "/vaultwarden/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def setup_vaultwarden(
    tenant_id: uuid.UUID,
    request: VaultwardenSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Trigger Vaultwarden setup playbook for a tenant.

    Creates a COMPOSITE task with the following steps:
    1. **ENV_UPDATE**: Set DOMAIN, ADMIN_TOKEN, SIGNUPS_ALLOWED
    2. **DOCKER_RELOAD**: Restart the Vaultwarden stack with pull=True
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if request.agent_id is not None:
        agent = await get_agent_by_id(db, request.agent_id)
        if agent is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Agent {request.agent_id} not found",
            )

    task = await create_vaultwarden_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=request.agent_id,
        domain=request.domain,
        admin_token=request.admin_token,
        signups_allowed=request.signups_allowed,
    )

    return task


# =============================================================================
# Portainer (no Playwright)
# =============================================================================


@router.post(
    "/portainer/setup",
    response_model=TaskResponse,
    status_code=status.HTTP_201_CREATED,
)
async def setup_portainer(
    tenant_id: uuid.UUID,
    request: PortainerSetupRequest,
    db: AsyncSessionDep,
) -> Task:
    """
    Trigger Portainer setup playbook for a tenant.

    Creates a COMPOSITE task with the following steps:
    1. **ENV_UPDATE**: Set PORTAINER_DOMAIN
    2. **DOCKER_RELOAD**: Restart the Portainer stack with pull=True
    """
    tenant = await get_tenant_by_id(db, tenant_id)
    if tenant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Tenant {tenant_id} not found",
        )

    if request.agent_id is not None:
        agent = await get_agent_by_id(db, request.agent_id)
        if agent is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Agent {request.agent_id} not found",
            )

    task = await create_portainer_setup_task(
        db=db,
        tenant_id=tenant_id,
        agent_id=request.agent_id,
        domain=request.domain,
    )

    return task
