# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Automation2 routes - node-types, execute, workflows, runs, tasks, connections, browse.
"""

import logging
from fastapi import APIRouter, Depends, Path, Query, Body, Request, HTTPException
from fastapi.responses import JSONResponse
from modules.auth import limiter, getRequestContext, RequestContext

from modules.features.automation2.mainAutomation2 import getAutomation2Services
from modules.features.automation2.nodeRegistry import getNodeTypesForApi
from modules.features.automation2.interfaceFeatureAutomation2 import getAutomation2Interface
from modules.workflows.automation2.executionEngine import executeGraph

logger = logging.getLogger(__name__)

router = APIRouter(
    prefix="/api/automation2",
    tags=["Automation2"],
    responses={404: {"description": "Not found"}, 403: {"description": "Forbidden"}},
)


def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
    """Validate user has access to the automation2 feature instance. Returns mandateId."""
    from fastapi import HTTPException
    from modules.interfaces.interfaceDbApp import getRootInterface

    rootInterface = getRootInterface()
    instance = rootInterface.getFeatureInstance(instanceId)
    if not instance:
        raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
    featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
    if not featureAccess or not featureAccess.enabled:
        raise HTTPException(status_code=403, detail="Access denied to this feature instance")
    return str(instance.mandateId) if instance.mandateId else ""


@router.get("/{instanceId}/info")
@limiter.limit("60/minute")
def get_automation2_info(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Minimal info endpoint - proves the feature works."""
    _validateInstanceAccess(instanceId, context)
    return {
        "featureCode": "automation2",
        "instanceId": instanceId,
        "status": "ok",
        "message": "Automation2 feature ready. Build from here.",
    }


@router.get("/{instanceId}/node-types")
@limiter.limit("60/minute")
def get_node_types(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    language: str = Query("en", description="Localization (en, de, fr)"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Return node types for the flow builder: static + I/O from methodDiscovery."""
    logger.info("automation2 node-types request: instanceId=%s language=%s", instanceId, language)
    mandateId = _validateInstanceAccess(instanceId, context)
    services = getAutomation2Services(
        context.user,
        mandateId=mandateId,
        featureInstanceId=instanceId,
    )
    result = getNodeTypesForApi(services, language=language)
    logger.info(
        "automation2 node-types response: %d nodeTypes %d categories",
        len(result.get("nodeTypes", [])),
        len(result.get("categories", [])),
    )
    return result


@router.post("/{instanceId}/execute")
@limiter.limit("30/minute")
async def post_execute(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    body: dict = Body(..., description="{ workflowId?, graph: { nodes, connections } }"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Execute automation2 graph. Body: { workflowId?, graph: { nodes, connections } }."""
    userId = str(context.user.id) if context.user else None
    logger.info(
        "automation2 execute request: instanceId=%s userId=%s body_keys=%s",
        instanceId,
        userId,
        list(body.keys()),
    )
    mandateId = _validateInstanceAccess(instanceId, context)
    services = getAutomation2Services(
        context.user,
        mandateId=mandateId,
        featureInstanceId=instanceId,
    )
    # Ensure workflow methods (outlook, ai, sharepoint, etc.) are discovered for ActionExecutor
    from modules.workflows.processing.shared.methodDiscovery import discoverMethods
    discoverMethods(services)

    graph = body.get("graph") or body
    workflowId = body.get("workflowId")
    req_nodes = graph.get("nodes") or []
    # When workflowId is set: prefer graph from request (current editor state) if it has nodes.
    # Only fall back to stored workflow graph when request graph is empty (e.g. resume from email).
    if workflowId and len(req_nodes) == 0:
        a2 = getAutomation2Interface(context.user, mandateId, instanceId)
        wf = a2.getWorkflow(workflowId)
        if wf and wf.get("graph"):
            graph = wf["graph"]
            logger.info("automation2 execute: loaded graph from workflow %s", workflowId)
    # Use transient workflowId when none provided (e.g. execute from editor without save)
    # Required for email.checkEmail pause/resume - run must be created
    if not workflowId:
        import uuid
        workflowId = f"transient-{uuid.uuid4().hex[:12]}"
        logger.info("automation2 execute: using transient workflowId=%s", workflowId)
    nodes_count = len(graph.get("nodes") or [])
    connections_count = len(graph.get("connections") or [])
    logger.info(
        "automation2 execute: graph nodes=%d connections=%d workflowId=%s mandateId=%s",
        nodes_count,
        connections_count,
        workflowId,
        mandateId,
    )
    a2_interface = getAutomation2Interface(context.user, mandateId, instanceId)
    result = await executeGraph(
        graph=graph,
        services=services,
        workflowId=workflowId,
        instanceId=instanceId,
        userId=userId,
        mandateId=mandateId,
        automation2_interface=a2_interface,
    )
    logger.info(
        "automation2 execute result: success=%s error=%s nodeOutputs_keys=%s failedNode=%s paused=%s",
        result.get("success"),
        result.get("error"),
        list(result.get("nodeOutputs", {}).keys()) if result.get("nodeOutputs") else [],
        result.get("failedNode"),
        result.get("paused"),
    )
    return result


# -------------------------------------------------------------------------
# Connections and Browse (for Email/SharePoint node config - like workspace)
# -------------------------------------------------------------------------


def _buildResolverDbInterface(chatService):
    """Build a DB adapter that ConnectorResolver can use to load UserConnections."""
    class _ResolverDbAdapter:
        def __init__(self, appInterface):
            self._app = appInterface

        def getUserConnection(self, connectionId: str):
            if hasattr(self._app, "getUserConnectionById"):
                return self._app.getUserConnectionById(connectionId)
            return None

    appIf = getattr(chatService, "interfaceDbApp", None)
    if appIf:
        return _ResolverDbAdapter(appIf)
    return getattr(chatService, "interfaceDbComponent", None)


@router.get("/{instanceId}/connections")
@limiter.limit("300/minute")
def list_automation2_connections(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Return the user's active connections (UserConnections) for Email/SharePoint node config."""
    mandateId = _validateInstanceAccess(instanceId, context)
    from modules.serviceCenter import getService
    from modules.serviceCenter.context import ServiceCenterContext
    ctx = ServiceCenterContext(
        user=context.user,
        mandate_id=str(context.mandateId) if context.mandateId else mandateId,
        feature_instance_id=instanceId,
    )
    chatService = getService("chat", ctx)
    connections = chatService.getUserConnections()
    items = []
    for c in connections or []:
        conn = c if isinstance(c, dict) else (c.model_dump() if hasattr(c, "model_dump") else {})
        authority = conn.get("authority")
        if hasattr(authority, "value"):
            authority = authority.value
        status = conn.get("status")
        if hasattr(status, "value"):
            status = status.value
        items.append({
            "id": conn.get("id"),
            "authority": authority,
            "externalUsername": conn.get("externalUsername"),
            "externalEmail": conn.get("externalEmail"),
            "status": status,
        })
    return {"connections": items}


@router.get("/{instanceId}/connections/{connectionId}/services")
@limiter.limit("120/minute")
async def list_connection_services(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    connectionId: str = Path(..., description="Connection ID"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Return the available services for a specific UserConnection."""
    mandateId = _validateInstanceAccess(instanceId, context)
    try:
        from modules.connectors.connectorResolver import ConnectorResolver
        from modules.serviceCenter import getService as getSvc
        from modules.serviceCenter.context import ServiceCenterContext
        ctx = ServiceCenterContext(
            user=context.user,
            mandate_id=str(context.mandateId) if context.mandateId else mandateId,
            feature_instance_id=instanceId,
        )
        chatService = getSvc("chat", ctx)
        securityService = getSvc("security", ctx)
        dbInterface = _buildResolverDbInterface(chatService)
        resolver = ConnectorResolver(securityService, dbInterface)
        provider = await resolver.resolve(connectionId)
        services = provider.getAvailableServices()
        _serviceLabels = {
            "sharepoint": "SharePoint",
            "outlook": "Outlook",
            "teams": "Teams",
            "onedrive": "OneDrive",
            "drive": "Google Drive",
            "gmail": "Gmail",
            "files": "Files (FTP)",
        }
        _serviceIcons = {
            "sharepoint": "sharepoint",
            "outlook": "mail",
            "teams": "chat",
            "onedrive": "cloud",
            "drive": "cloud",
            "gmail": "mail",
            "files": "folder",
        }
        items = [
            {"service": s, "label": _serviceLabels.get(s, s), "icon": _serviceIcons.get(s, "folder")}
            for s in services
        ]
        return {"services": items}
    except Exception as e:
        logger.error(f"Error listing services for connection {connectionId}: {e}")
        return JSONResponse({"services": [], "error": str(e)}, status_code=400)


@router.get("/{instanceId}/connections/{connectionId}/browse")
@limiter.limit("300/minute")
async def browse_connection_service(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    connectionId: str = Path(..., description="Connection ID"),
    service: str = Query(..., description="Service name (e.g. sharepoint, onedrive, outlook)"),
    path: str = Query("/", description="Path within the service to browse"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Browse folders/items within a connection's service at a given path."""
    mandateId = _validateInstanceAccess(instanceId, context)
    try:
        from modules.connectors.connectorResolver import ConnectorResolver
        from modules.serviceCenter import getService as getSvc
        from modules.serviceCenter.context import ServiceCenterContext
        ctx = ServiceCenterContext(
            user=context.user,
            mandate_id=str(context.mandateId) if context.mandateId else mandateId,
            feature_instance_id=instanceId,
        )
        chatService = getSvc("chat", ctx)
        securityService = getSvc("security", ctx)
        dbInterface = _buildResolverDbInterface(chatService)
        resolver = ConnectorResolver(securityService, dbInterface)
        adapter = await resolver.resolveService(connectionId, service)
        entries = await adapter.browse(path, filter=None)
        items = []
        for entry in (entries or []):
            items.append({
                "name": entry.name,
                "path": entry.path,
                "isFolder": entry.isFolder,
                "size": entry.size,
                "mimeType": entry.mimeType,
                "metadata": entry.metadata if hasattr(entry, "metadata") else {},
            })
        return {"items": items, "path": path, "service": service}
    except Exception as e:
        logger.error(f"Error browsing {service} for connection {connectionId} at '{path}': {e}")
        return JSONResponse({"items": [], "error": str(e)}, status_code=400)


# -------------------------------------------------------------------------
# Workflow CRUD
# -------------------------------------------------------------------------


def _get_node_label_from_graph(graph: dict, nodeId: str) -> str:
    """Extract human-readable label for a node from graph."""
    if not graph or not nodeId:
        return nodeId or ""
    nodes = graph.get("nodes") or []
    for n in nodes:
        if n.get("id") == nodeId:
            params = n.get("parameters") or {}
            config = params.get("config") or {}
            if isinstance(config, dict):
                label = config.get("title") or config.get("label")
            else:
                label = None
            return (
                n.get("title")
                or label
                or params.get("title")
                or params.get("label")
                or n.get("type", "")
                or nodeId
            )
    return nodeId or ""


@router.get("/{instanceId}/workflows")
@limiter.limit("60/minute")
def get_workflows(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """List all workflows for this feature instance.
    Enriches each workflow with runCount, isRunning, stuckAtNodeId, stuckAtNodeLabel,
    createdAt, lastStartedAt.
    """
    mandateId = _validateInstanceAccess(instanceId, context)
    a2 = getAutomation2Interface(context.user, mandateId, instanceId)
    items = a2.getWorkflows()
    enriched = []
    for wf in items:
        wf_id = wf.get("id")
        runs = a2.getRunsByWorkflow(wf_id) if wf_id else []
        run_count = len(runs)
        active_run = None
        last_started_at = None
        for r in runs:
            ts = r.get("sysCreatedAt")
            if ts and (last_started_at is None or ts > last_started_at):
                last_started_at = ts
            if r.get("status") in ("running", "paused"):
                active_run = r
        stuck_at_node_id = active_run.get("currentNodeId") if active_run else None
        stuck_at_node_label = ""
        if stuck_at_node_id and wf.get("graph"):
            stuck_at_node_label = _get_node_label_from_graph(wf["graph"], stuck_at_node_id)
        enriched.append({
            **wf,
            "runCount": run_count,
            "isRunning": active_run is not None,
            "runStatus": active_run.get("status") if active_run else None,
            "stuckAtNodeId": stuck_at_node_id,
            "stuckAtNodeLabel": stuck_at_node_label or stuck_at_node_id or "",
            "createdAt": wf.get("sysCreatedAt"),
            "lastStartedAt": last_started_at,
        })
    return {"workflows": enriched}


@router.get("/{instanceId}/workflows/{workflowId}")
@limiter.limit("60/minute")
def get_workflow(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    workflowId: str = Path(..., description="Workflow ID"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Get a single workflow by ID."""
    mandateId = _validateInstanceAccess(instanceId, context)
    a2 = getAutomation2Interface(context.user, mandateId, instanceId)
    wf = a2.getWorkflow(workflowId)
    if not wf:
        raise HTTPException(status_code=404, detail="Workflow not found")
    return wf


@router.post("/{instanceId}/workflows")
@limiter.limit("30/minute")
def create_workflow(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    body: dict = Body(..., description="{ label, graph }"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Create a new workflow."""
    mandateId = _validateInstanceAccess(instanceId, context)
    a2 = getAutomation2Interface(context.user, mandateId, instanceId)
    created = a2.createWorkflow(body)
    return created


@router.put("/{instanceId}/workflows/{workflowId}")
@limiter.limit("30/minute")
def update_workflow(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    workflowId: str = Path(..., description="Workflow ID"),
    body: dict = Body(..., description="{ label?, graph? }"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Update a workflow."""
    mandateId = _validateInstanceAccess(instanceId, context)
    a2 = getAutomation2Interface(context.user, mandateId, instanceId)
    updated = a2.updateWorkflow(workflowId, body)
    if not updated:
        raise HTTPException(status_code=404, detail="Workflow not found")
    return updated


@router.delete("/{instanceId}/workflows/{workflowId}")
@limiter.limit("30/minute")
def delete_workflow(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    workflowId: str = Path(..., description="Workflow ID"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Delete a workflow."""
    mandateId = _validateInstanceAccess(instanceId, context)
    a2 = getAutomation2Interface(context.user, mandateId, instanceId)
    if not a2.deleteWorkflow(workflowId):
        raise HTTPException(status_code=404, detail="Workflow not found")
    return {"success": True}


# -------------------------------------------------------------------------
# Runs and Resume
# -------------------------------------------------------------------------


@router.get("/{instanceId}/workflows/{workflowId}/runs")
@limiter.limit("60/minute")
def get_workflow_runs(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    workflowId: str = Path(..., description="Workflow ID"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Get runs for a workflow."""
    mandateId = _validateInstanceAccess(instanceId, context)
    a2 = getAutomation2Interface(context.user, mandateId, instanceId)
    if not a2.getWorkflow(workflowId):
        raise HTTPException(status_code=404, detail="Workflow not found")
    runs = a2.getRunsByWorkflow(workflowId)
    return {"runs": runs}


@router.post("/{instanceId}/runs/{runId}/resume")
@limiter.limit("30/minute")
async def resume_run(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    runId: str = Path(..., description="Run ID"),
    body: dict = Body(..., description="{ taskId, result }"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Resume a paused run after task completion."""
    mandateId = _validateInstanceAccess(instanceId, context)
    a2 = getAutomation2Interface(context.user, mandateId, instanceId)
    run = a2.getRun(runId)
    if not run:
        raise HTTPException(status_code=404, detail="Run not found")
    taskId = body.get("taskId")
    result = body.get("result")
    if not taskId or result is None:
        raise HTTPException(status_code=400, detail="taskId and result required")
    task = a2.getTask(taskId)
    if not task or task.get("runId") != runId:
        raise HTTPException(status_code=404, detail="Task not found")
    if task.get("status") != "pending":
        raise HTTPException(status_code=400, detail="Task already completed")
    a2.updateTask(taskId, status="completed", result=result)
    nodeId = task.get("nodeId")
    nodeOutputs = dict(run.get("nodeOutputs") or {})
    nodeOutputs[nodeId] = result
    runContext = run.get("context") or {}
    connectionMap = runContext.get("connectionMap", {})
    inputSources = runContext.get("inputSources", {})
    workflowId = run.get("workflowId")
    wf = a2.getWorkflow(workflowId) if workflowId else None
    if not wf or not wf.get("graph"):
        raise HTTPException(status_code=400, detail="Workflow graph not found")
    graph = wf["graph"]
    services = getAutomation2Services(context.user, mandateId=mandateId, featureInstanceId=instanceId)
    resume_result = await executeGraph(
        graph=graph,
        services=services,
        workflowId=workflowId,
        instanceId=instanceId,
        userId=str(context.user.id) if context.user else None,
        mandateId=mandateId,
        automation2_interface=a2,
        initialNodeOutputs=nodeOutputs,
        startAfterNodeId=nodeId,
        runId=runId,
    )
    return resume_result


# -------------------------------------------------------------------------
# Tasks
# -------------------------------------------------------------------------


@router.get("/{instanceId}/tasks")
@limiter.limit("60/minute")
def get_tasks(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    workflowId: str = Query(None, description="Filter by workflow ID"),
    status: str = Query(None, description="Filter: pending, completed, rejected"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Get tasks - by default those assigned to current user, or all if no assignee filter.
    Enriches each task with workflowLabel and createdAt (from sysCreatedAt).
    """
    mandateId = _validateInstanceAccess(instanceId, context)
    a2 = getAutomation2Interface(context.user, mandateId, instanceId)
    assigneeId = str(context.user.id) if context.user else None
    items = a2.getTasks(workflowId=workflowId, status=status, assigneeId=assigneeId)
    workflows = {w["id"]: w for w in a2.getWorkflows()}
    enriched = []
    for t in items:
        wf = workflows.get(t.get("workflowId") or "")
        enriched.append({
            **t,
            "workflowLabel": wf.get("label", t.get("workflowId", "")) if wf else t.get("workflowId", ""),
            "createdAt": t.get("sysCreatedAt"),
        })
    return {"tasks": enriched}


@router.post("/{instanceId}/tasks/{taskId}/complete")
@limiter.limit("30/minute")
async def complete_task(
    request: Request,
    instanceId: str = Path(..., description="Feature instance ID"),
    taskId: str = Path(..., description="Task ID"),
    body: dict = Body(..., description="{ result }"),
    context: RequestContext = Depends(getRequestContext),
) -> dict:
    """Complete a task and resume the run."""
    mandateId = _validateInstanceAccess(instanceId, context)
    a2 = getAutomation2Interface(context.user, mandateId, instanceId)
    task = a2.getTask(taskId)
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")
    runId = task.get("runId")
    result = body.get("result")
    if result is None:
        raise HTTPException(status_code=400, detail="result required")
    run = a2.getRun(runId)
    if not run:
        raise HTTPException(status_code=404, detail="Run not found")
    if task.get("status") != "pending":
        raise HTTPException(status_code=400, detail="Task already completed")
    a2.updateTask(taskId, status="completed", result=result)
    nodeId = task.get("nodeId")
    nodeOutputs = dict(run.get("nodeOutputs") or {})
    nodeOutputs[nodeId] = result
    workflowId = run.get("workflowId")
    wf = a2.getWorkflow(workflowId) if workflowId else None
    if not wf or not wf.get("graph"):
        raise HTTPException(status_code=400, detail="Workflow graph not found")
    graph = wf["graph"]
    services = getAutomation2Services(context.user, mandateId=mandateId, featureInstanceId=instanceId)
    return await executeGraph(
        graph=graph,
        services=services,
        workflowId=workflowId,
        instanceId=instanceId,
        userId=str(context.user.id) if context.user else None,
        mandateId=mandateId,
        automation2_interface=a2,
        initialNodeOutputs=nodeOutputs,
        startAfterNodeId=nodeId,
        runId=runId,
    )
