"""Callback management API endpoints."""

import asyncio
import logging
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, Field

from ..auth import verify_token
from ..config import CallbackConfig, settings
from ..config_manager import config_manager
from ..services.rate_limit import check as ratelimit_check
from ..services.rate_limit import get_peer

router = APIRouter(prefix="/api/callbacks", tags=["callbacks"])
logger = logging.getLogger(__name__)

# Dedicated executor for callback/subprocess execution
_callback_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="callback")


def shutdown_callback_executor() -> None:
    """Shut down the callback executor cleanly on application teardown."""
    _callback_executor.shutdown(wait=False, cancel_futures=True)


def _require_callbacks_management() -> None:
    """Authorise a callbacks-CRUD operation. Operator flag + per-token admin scope."""
    if not settings.callbacks_management:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=(
                "Callbacks management is disabled. Set callbacks_management: true"
                " in config.yaml to enable."
            ),
        )
    from ..auth import auth_enabled, token_has_scope, token_label_var
    if auth_enabled():
        label = token_label_var.get("unknown")
        if not token_has_scope(label, "admin"):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Token '{label}' lacks required scope: admin",
            )


class CallbackInfo(BaseModel):
    """Information about a configured callback."""

    name: str
    command: str
    timeout: int
    working_dir: str | None = None
    shell: bool


class CallbackCreateRequest(BaseModel):
    """Request model for creating or updating a callback."""

    command: str = Field(..., description="Command to execute", min_length=1)
    timeout: int = Field(default=30, description="Execution timeout in seconds", ge=1, le=300)
    working_dir: str | None = Field(default=None, description="Working directory")
    shell: bool = Field(default=True, description="Run command in shell")


class CallbackExecuteResponse(BaseModel):
    """Response model for callback execution."""

    success: bool
    callback: str
    exit_code: int | None = None
    stdout: str = ""
    stderr: str = ""
    error: str | None = None
    execution_time: float | None = None


def _validate_callback_name(name: str) -> None:
    """Validate callback name.

    Args:
        name: Callback name to validate.

    Raises:
        HTTPException: If name is invalid.
    """
    # All available callback events
    valid_names = {
        "on_play",
        "on_pause",
        "on_stop",
        "on_next",
        "on_previous",
        "on_volume",
        "on_mute",
        "on_seek",
        "on_turn_on",
        "on_turn_off",
        "on_toggle",
    }

    if name not in valid_names:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Callback name must be one of: {', '.join(sorted(valid_names))}",
        )


@router.get("/list")
async def list_callbacks(_: str = Depends(verify_token)) -> list[CallbackInfo]:
    """List all configured callbacks.

    Returns:
        List of configured callbacks.
    """
    return [
        CallbackInfo(
            name=name,
            command=config.command,
            timeout=config.timeout,
            working_dir=config.working_dir,
            shell=config.shell,
        )
        for name, config in settings.callbacks.items()
    ]


@router.post("/execute/{callback_name}")
async def execute_callback(
    callback_name: str,
    http_request: Request,
    _: str = Depends(verify_token),
) -> CallbackExecuteResponse:
    """Execute a callback for debugging purposes.

    Args:
        callback_name: Name of the callback to execute

    Returns:
        Execution result including stdout, stderr, and exit code
    """
    # Rate-limit callback execution per peer (10/min) — callbacks also run
    # subprocesses and need the same protection as scripts.
    allowed, retry_after = ratelimit_check("execute", get_peer(http_request))
    if not allowed:
        raise HTTPException(
            status_code=429,
            detail="Too many callback executions, slow down",
            headers={"Retry-After": str(int(retry_after or 60))},
        )

    # Validate callback name
    _validate_callback_name(callback_name)

    # Check if callback exists
    if callback_name not in settings.callbacks:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Callback '{callback_name}' not found. Use /api/callbacks/list to see configured callbacks.",
        )

    callback_config = settings.callbacks[callback_name]

    logger.info(f"Executing callback for debugging: {callback_name}")

    from ..services.audit_log import record_script_execution

    try:
        # Execute in dedicated thread pool to not block the default executor
        loop = asyncio.get_running_loop()
        result = await loop.run_in_executor(
            _callback_executor,
            lambda: _run_callback(
                command=callback_config.command,
                timeout=callback_config.timeout,
                shell=callback_config.shell,
                working_dir=callback_config.working_dir,
            ),
        )

        record_script_execution(
            kind="callback",
            name=callback_name,
            exit_code=result["exit_code"],
            duration=result.get("execution_time"),
            stdout=result.get("stdout"),
            stderr=result.get("stderr"),
        )

        return CallbackExecuteResponse(
            success=result["exit_code"] == 0,
            callback=callback_name,
            exit_code=result["exit_code"],
            stdout=result["stdout"],
            stderr=result["stderr"],
            execution_time=result.get("execution_time"),
        )

    except Exception as e:
        logger.error(f"Callback execution error: {e}")
        record_script_execution(
            kind="callback",
            name=callback_name,
            exit_code=None,
            duration=None,
            error=str(e),
        )
        return CallbackExecuteResponse(
            success=False,
            callback=callback_name,
            error=str(e),
        )


def _run_callback(
    command: str,
    timeout: int,
    shell: bool,
    working_dir: str | None,
) -> dict[str, Any]:
    """Run a callback synchronously.

    Args:
        command: Command to execute
        timeout: Timeout in seconds
        shell: Whether to run in shell
        working_dir: Working directory

    Returns:
        Dict with exit_code, stdout, stderr, execution_time
    """
    start_time = time.time()
    popen_kwargs: dict[str, Any] = {}
    if sys.platform == "win32":
        popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
    else:
        popen_kwargs["start_new_session"] = True
    try:
        result = subprocess.run(
            command,
            shell=shell,
            cwd=working_dir,
            capture_output=True,
            text=True,
            timeout=timeout,
            **popen_kwargs,
        )
        execution_time = time.time() - start_time
        return {
            "exit_code": result.returncode,
            "stdout": result.stdout[:10000],  # Limit output size
            "stderr": result.stderr[:10000],
            "execution_time": execution_time,
        }
    except subprocess.TimeoutExpired:
        execution_time = time.time() - start_time
        return {
            "exit_code": -1,
            "stdout": "",
            "stderr": f"Callback timed out after {timeout} seconds",
            "execution_time": execution_time,
        }
    except Exception as e:
        execution_time = time.time() - start_time
        return {
            "exit_code": -1,
            "stdout": "",
            "stderr": str(e),
            "execution_time": execution_time,
        }


@router.post("/create/{callback_name}")
async def create_callback(
    callback_name: str,
    request: CallbackCreateRequest,
    _: str = Depends(verify_token),
) -> dict[str, Any]:
    """Create a new callback.

    Args:
        callback_name: Callback event name (on_turn_on, on_turn_off, on_toggle).
        request: Callback configuration.

    Returns:
        Success response with callback name.

    Raises:
        HTTPException: If callback already exists or name is invalid.
    """
    _require_callbacks_management()
    _validate_callback_name(callback_name)

    # Check if callback already exists
    if callback_name in settings.callbacks:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=(
                f"Callback '{callback_name}' already exists."
                f" Use PUT /api/callbacks/update/{callback_name} to update it."
            ),
        )

    # Create callback config
    callback_config = CallbackConfig(**request.model_dump())

    # Add to config file and in-memory
    try:
        config_manager.add_callback(callback_name, callback_config)
    except Exception as e:
        logger.error(f"Failed to add callback '{callback_name}': {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to add callback: {str(e)}",
        )

    logger.info(f"Callback '{callback_name}' created successfully")
    return {"success": True, "callback": callback_name}


@router.put("/update/{callback_name}")
async def update_callback(
    callback_name: str,
    request: CallbackCreateRequest,
    _: str = Depends(verify_token),
) -> dict[str, Any]:
    """Update an existing callback.

    Args:
        callback_name: Callback event name.
        request: Updated callback configuration.

    Returns:
        Success response with callback name.

    Raises:
        HTTPException: If callback does not exist.
    """
    _require_callbacks_management()
    _validate_callback_name(callback_name)

    # Check if callback exists
    if callback_name not in settings.callbacks:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=(
                f"Callback '{callback_name}' not found."
                f" Use POST /api/callbacks/create/{callback_name} to create it."
            ),
        )

    # Create updated callback config
    callback_config = CallbackConfig(**request.model_dump())

    # Update config file and in-memory
    try:
        config_manager.update_callback(callback_name, callback_config)
    except Exception as e:
        logger.error(f"Failed to update callback '{callback_name}': {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to update callback: {str(e)}",
        )

    logger.info(f"Callback '{callback_name}' updated successfully")
    return {"success": True, "callback": callback_name}


@router.delete("/delete/{callback_name}")
async def delete_callback(
    callback_name: str,
    _: str = Depends(verify_token),
) -> dict[str, Any]:
    """Delete a callback.

    Args:
        callback_name: Callback event name.

    Returns:
        Success response with callback name.

    Raises:
        HTTPException: If callback does not exist.
    """
    _require_callbacks_management()
    _validate_callback_name(callback_name)

    # Check if callback exists
    if callback_name not in settings.callbacks:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Callback '{callback_name}' not found",
        )

    # Delete from config file and in-memory
    try:
        config_manager.delete_callback(callback_name)
    except Exception as e:
        logger.error(f"Failed to delete callback '{callback_name}': {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to delete callback: {str(e)}",
        )

    logger.info(f"Callback '{callback_name}' deleted successfully")
    return {"success": True, "callback": callback_name}
