"""Nexus — Scripts API Routes (Script library CRUD + command execution)
Presentation layer — receives HTTP requests, delegates to ScriptService.
All operations require JWT authentication.
"""

import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Request

from server.config import settings
from server.api.dependencies import get_script_service, get_db
from server.api.auth_jwt import get_current_admin
from server.api.schemas import (
    ScriptCreate, ScriptUpdate, ScriptExecute, ScriptExecutionRetry,
    ScriptExecutionMarkStuck, DbCredentialCreate, DbCredentialUpdate,
)
from server.application.services.script_service import ScriptService
from server.domain.models import Script, DbCredential, Admin, AuditLog
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl

from sqlalchemy.ext.asyncio import AsyncSession

router = APIRouter(prefix="/api/scripts", tags=["scripts"])

logger = logging.getLogger("nexus.scripts")


# ── Script CRUD ──

@router.get("/", response_model=list)
async def list_scripts(
    category: Optional[str] = Query(None, description="Filter by category"),
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
):
    """List all scripts, optionally filtered by category"""
    scripts = await service.list_scripts(category)
    return [_script_to_dict(s) for s in scripts]


@router.get("/exec-config", response_model=dict)
async def script_exec_config(
    admin: Admin = Depends(get_current_admin),
):
    """Limits for script execution UI (built-in batching)."""
    return {
        "batch_size": settings.SCRIPT_EXEC_BATCH_SIZE,
        "concurrency": settings.SCRIPT_EXEC_CONCURRENCY,
        "max_timeout": 600,
    }


# ── DB Credentials (must register before /{id}) ──

@router.get("/credentials", response_model=list)
async def list_credentials(
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
):
    """List all database credentials"""
    credentials = await service.list_credentials()
    return [_credential_to_dict(c) for c in credentials]


@router.post("/credentials", response_model=dict, status_code=201)
async def create_credential(
    payload: DbCredentialCreate,
    request: Request,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
    db: AsyncSession = Depends(get_db),
):
    """Create a database credential (password will be encrypted)"""
    data = payload.model_dump(exclude_none=True)
    data["encrypted_password"] = data.pop("password", "")
    credential = DbCredential(**data)
    created = await service.create_credential(credential)

    audit_repo = AuditLogRepositoryImpl(db)
    await audit_repo.create(AuditLog(
        admin_username=admin.username, action="create_credential",
        target_type="credential", target_id=created.id,
        detail=f"名称={created.name}", ip_address=request.client.host if request.client else "",
    ))

    return _credential_to_dict(created)


@router.put("/credentials/{id}", response_model=dict)
async def update_credential(
    id: int,
    payload: DbCredentialUpdate,
    request: Request,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
    db: AsyncSession = Depends(get_db),
):
    """Update a database credential (password optional — omit to keep existing)"""
    from server.infrastructure.database.db_credential_repo import DbCredentialRepositoryImpl
    from server.infrastructure.database.crypto import encrypt_value

    repo = DbCredentialRepositoryImpl(db)
    credential = await repo.get_by_id(id)
    if not credential:
        raise HTTPException(status_code=404, detail="Credential not found")

    updates = payload.model_dump(exclude_unset=True)
    # Handle password separately — encrypt if provided, skip if omitted
    new_password = updates.pop("password", None)
    # Explicit allowlist to prevent mass-assignment to sensitive columns
    ALLOWED_FIELDS = {"name", "db_type", "host", "port", "username", "database"}
    for key, value in updates.items():
        if key in ALLOWED_FIELDS and hasattr(credential, key):
            setattr(credential, key, value)
    if new_password:
        credential.encrypted_password = encrypt_value(new_password)

    updated = await repo.update(credential)

    audit_repo = AuditLogRepositoryImpl(db)
    await audit_repo.create(AuditLog(
        admin_username=admin.username, action="update_credential",
        target_type="credential", target_id=id,
        detail=f"名称={updated.name}", ip_address=request.client.host if request.client else "",
    ))

    return _credential_to_dict(updated)


@router.delete("/credentials/{id}", status_code=204)
async def delete_credential(
    id: int,
    request: Request,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
    db: AsyncSession = Depends(get_db),
):
    """Delete a database credential"""
    result = await service.delete_credential(id)
    if not result:
        raise HTTPException(status_code=404, detail="Credential not found")

    audit_repo = AuditLogRepositoryImpl(db)
    await audit_repo.create(AuditLog(
        admin_username=admin.username, action="delete_credential",
        target_type="credential", target_id=id,
        ip_address=request.client.host if request.client else "",
    ))


@router.get("/executions", response_model=dict)
async def list_executions(
    limit: int = Query(50, ge=1, le=200),
    offset: int = Query(0, ge=0),
    status: Optional[str] = Query(None, description="Filter: running/completed/failed/partial"),
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
):
    """List script execution history (newest first)."""
    return await service.list_executions(limit=limit, offset=offset, status=status)


@router.post("/executions/{id}/stop", response_model=dict)
async def stop_execution(
    id: int,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
):
    """Stop pending long-task processes on child hosts."""
    execution = await service.stop_execution(id, operator=admin.username)
    if not execution:
        raise HTTPException(status_code=404, detail="Execution not found")
    detail = await service.get_execution_detail(id)
    return detail if detail else _execution_to_dict(execution)


@router.post("/executions/{id}/mark-stuck", response_model=dict)
async def mark_execution_stuck(
    id: int,
    payload: ScriptExecutionMarkStuck,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
):
    """Mark execution as suspected stuck — event timeline + MySQL + audit."""
    execution = await service.mark_execution_stuck(
        id, operator=admin.username, detail=payload.detail,
    )
    if not execution:
        raise HTTPException(status_code=404, detail="Execution not found")
    detail = await service.get_execution_detail(id)
    return detail or _execution_to_dict(execution)


@router.post("/executions/{id}/retry", response_model=dict, status_code=201)
async def retry_execution(
    id: int,
    payload: ScriptExecutionRetry,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
):
    """Re-run failed / pending / cancelled servers from a previous execution."""
    from server.api.dependencies import check_dangerous_command

    prev = await service.get_execution(id)
    if not prev:
        raise HTTPException(status_code=404, detail="Execution not found")
    command, _ = service._parse_execution_meta(prev.command or "")
    warnings = check_dangerous_command(command)
    if warnings:
        raise HTTPException(
            status_code=400,
            detail={"message": "危险命令已拒绝执行", "warnings": warnings},
        )
    try:
        execution = await service.retry_execution(
            id,
            server_ids=payload.server_ids,
            credential_id=payload.credential_id,
            timeout=payload.timeout,
            long_task=payload.long_task,
            operator=admin.username,
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e)) from e
    detail = await service.get_execution_detail(execution.id)
    if detail:
        detail["parent_execution_id"] = id
        return detail
    out = _execution_to_dict(execution)
    out["parent_execution_id"] = id
    return out


@router.get("/executions/{id}", response_model=dict)
async def get_execution(
    id: int,
    fetch_logs: bool = Query(
        False,
        description="Tail /var/log/nexus-job/*.log on child hosts (long tasks)",
    ),
    log_lines: int = Query(100, ge=10, le=500),
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
):
    """Get execution result; optional live log tail from managed servers."""
    detail = await service.get_execution_detail(
        id, fetch_logs=fetch_logs, log_tail_lines=log_lines,
    )
    if not detail:
        raise HTTPException(status_code=404, detail="Execution not found")
    return detail


@router.get("/{id}", response_model=dict)
async def get_script(
    id: int,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
):
    """Get a single script by ID"""
    script = await service.get_script(id)
    if not script:
        raise HTTPException(status_code=404, detail="Script not found")
    return _script_to_dict(script)


@router.post("/", response_model=dict, status_code=201)
async def create_script(
    payload: ScriptCreate,
    request: Request,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
    db: AsyncSession = Depends(get_db),
):
    """Create a new script"""
    script = Script(**payload.model_dump(exclude_none=True))
    created = await service.create_script(script)

    audit_repo = AuditLogRepositoryImpl(db)
    await audit_repo.create(AuditLog(
        admin_username=admin.username, action="create_script",
        target_type="script", target_id=created.id,
        detail=f"名称={created.name}", ip_address=request.client.host if request.client else "",
    ))

    return _script_to_dict(created)


@router.put("/{id}", response_model=dict)
async def update_script(
    id: int,
    payload: ScriptUpdate,
    request: Request,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
    db: AsyncSession = Depends(get_db),
):
    """Update an existing script"""
    script = await service.get_script(id)
    if not script:
        raise HTTPException(status_code=404, detail="Script not found")
    # Explicit allowlist to prevent mass-assignment to sensitive columns
    ALLOWED_FIELDS = {"name", "category", "content", "description"}
    for key, value in payload.model_dump(exclude_unset=True).items():
        if key in ALLOWED_FIELDS:
            setattr(script, key, value)
    updated = await service.update_script(script)

    audit_repo = AuditLogRepositoryImpl(db)
    await audit_repo.create(AuditLog(
        admin_username=admin.username, action="update_script",
        target_type="script", target_id=id,
        detail=f"名称={updated.name}", ip_address=request.client.host if request.client else "",
    ))

    return _script_to_dict(updated)


@router.delete("/{id}", status_code=204)
async def delete_script(
    id: int,
    request: Request,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
    db: AsyncSession = Depends(get_db),
):
    """Delete a script"""
    script = await service.get_script(id)
    if not script:
        raise HTTPException(status_code=404, detail="Script not found")
    result = await service.delete_script(id)
    if not result:
        raise HTTPException(status_code=404, detail="Script not found")

    audit_repo = AuditLogRepositoryImpl(db)
    await audit_repo.create(AuditLog(
        admin_username=admin.username, action="delete_script",
        target_type="script", target_id=id,
        detail=f"名称={script.name}", ip_address=request.client.host if request.client else "",
    ))


# ── Command Execution ──

@router.post("/exec", response_model=dict)
async def execute_command(
    payload: ScriptExecute,
    admin: Admin = Depends(get_current_admin),
    service: ScriptService = Depends(get_script_service),
):
    """Execute a shell command on multiple servers via Agent /api/exec"""
    # P2-18: Log dangerous commands for audit trail
    from server.api.dependencies import check_dangerous_command
    warnings = check_dangerous_command(payload.command)
    if warnings:
        raise HTTPException(
            status_code=400,
            detail={"message": "危险命令已拒绝执行", "warnings": warnings},
        )

    try:
        execution = await service.execute_command(
            command=payload.command,
            server_ids=payload.server_ids,
            script_id=payload.script_id,
            credential_id=payload.credential_id,
            timeout=payload.timeout,
            operator=admin.username,
            long_task=payload.long_task,
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e)) from e
    detail = await service.get_execution_detail(execution.id)
    return detail if detail else _execution_to_dict(execution)


# ── Helper functions ──

def _script_to_dict(script: Script) -> dict:
    return {
        "id": script.id,
        "name": script.name,
        "category": script.category,
        "content": script.content,
        "description": script.description,
        "created_by": script.created_by,
        "created_at": str(script.created_at) if script.created_at else None,
        "updated_at": str(script.updated_at) if script.updated_at else None,
    }


def _execution_to_dict(execution) -> dict:
    return {
        "id": execution.id,
        "script_id": execution.script_id,
        "command": execution.command,
        "server_ids": execution.server_ids,
        "status": execution.status,
        "results": execution.results,
        "operator": execution.operator,
        "credential_id": getattr(execution, "credential_id", None),
        "started_at": str(execution.started_at) if execution.started_at else None,
        "completed_at": str(execution.completed_at) if execution.completed_at else None,
    }


def _credential_to_dict(credential: DbCredential) -> dict:
    """Convert credential to dict (never expose password in API response)"""
    return {
        "id": credential.id,
        "name": credential.name,
        "db_type": credential.db_type,
        "host": credential.host,
        "port": credential.port,
        "username": credential.username,
        "database": credential.database,
        "created_at": str(credential.created_at) if credential.created_at else None,
    }
