"""Dynamic FastAPI endpoints generated from workflow schemas."""

import inspect
from typing import Any, Dict, List, Optional, get_type_hints
from fastapi import HTTPException, Query, Path as PathParam, Body, File, Form, Header, Cookie, UploadFile
from pydantic import BaseModel, create_model, Field

import ombra
from ombra.execution import get_execution_manager
from ombra.persistence import _make_serializable


# Type mapping from string names to Python types
TYPE_MAP = {
    'int': int,
    'str': str,
    'float': float,
    'bool': bool,
    'list': list,
    'dict': dict,
    'Any': Any,
    'UploadFile': UploadFile,
}

# Mapping from FastAPI annotation names to their actual classes
FASTAPI_ANNOTATION_MAP = {
    'File': File,
    'Form': Form,
    'Body': Body,
    'Query': Query,
    'Path': PathParam,
    'Header': Header,
    'Cookie': Cookie,
}


def create_pydantic_model_from_schema(workflow_name: str, parameters: List[Dict[str, Any]]) -> BaseModel:
    """Create a Pydantic model from a workflow parameter schema.

    Args:
        workflow_name: Name of the workflow (for model name)
        parameters: List of parameter dicts with name, type, required, default, fastapi_annotation

    Returns:
        Pydantic model class
    """
    fields = {}

    for param in parameters:
        param_name = param['name']
        param_type_str = param['type']
        required = param['required']
        default_value = param.get('default')
        fastapi_annotation = param.get('fastapi_annotation')

        # Map string type to Python type
        param_type = TYPE_MAP.get(param_type_str, Any)

        # Build field definition
        # If there's a FastAPI annotation (File, Form, etc.), use it
        if fastapi_annotation and fastapi_annotation in FASTAPI_ANNOTATION_MAP:
            # Reconstruct the FastAPI parameter annotation
            annotation_class = FASTAPI_ANNOTATION_MAP[fastapi_annotation]

            # For required parameters with FastAPI annotation, use annotation_class(...)
            # For optional, use annotation_class(default_value) or just annotation_class(...)
            if required:
                fields[param_name] = (param_type, annotation_class(...))
            else:
                # If there's a meaningful default (not just a string representation), use it
                # Otherwise, just use annotation_class(...)
                fields[param_name] = (param_type, annotation_class(...))
        else:
            # Standard parameter without FastAPI annotation
            if required:
                fields[param_name] = (param_type, ...)
            else:
                fields[param_name] = (param_type, default_value)

    # Create and return Pydantic model
    model_name = f"{workflow_name.capitalize()}Input"
    return create_model(model_name, **fields)


def _has_file_or_form_params(parameters: List[Dict[str, Any]]) -> bool:
    """Check if parameters include File or Form annotations."""
    for param in parameters:
        annotation = param.get('fastapi_annotation')
        if annotation in ['File', 'Form']:
            return True
    return False


def _create_dynamic_function_signature(parameters: List[Dict[str, Any]]):
    """Create a dynamic function with the correct parameter signature.

    For workflows with File/Form parameters, this creates individual function params.
    Returns a tuple of (param_names, param_annotations, param_defaults).
    """
    from inspect import Parameter

    param_specs = []

    for param in parameters:
        param_name = param['name']
        param_type_str = param['type']
        fastapi_annotation = param.get('fastapi_annotation')
        required = param['required']

        # Get the type
        param_type = TYPE_MAP.get(param_type_str, Any)

        # Get the default/annotation
        if fastapi_annotation and fastapi_annotation in FASTAPI_ANNOTATION_MAP:
            annotation_class = FASTAPI_ANNOTATION_MAP[fastapi_annotation]
            default = annotation_class(...)
        elif required:
            default = Parameter.empty
        else:
            default = param.get('default')

        param_specs.append((param_name, param_type, default))

    return param_specs


def create_workflow_endpoints(app, schemas: Optional[List[Dict[str, Any]]] = None) -> None:
    """Create dynamic API endpoints from workflow schemas.

    This generates one endpoint per workflow with proper input validation.
    Schemas are loaded from the database, so endpoints survive server reloads.

    Args:
        app: FastAPI application instance
        schemas: Optional list of workflow schemas (if None, loads from DB)
    """
    execution_manager = get_execution_manager()

    # Load schemas from database if not provided
    if schemas is None:
        schemas = execution_manager.db.load_workflow_schemas()

    # Generate endpoints for each workflow schema
    for schema in schemas:
        workflow_name = schema['workflow_name']
        description = schema.get('description', '')
        parameters = schema['parameters']

        # Check if we have File/Form parameters
        has_multipart = _has_file_or_form_params(parameters)

        if has_multipart:
            # Create endpoint with individual parameters for file uploads
            param_specs = _create_dynamic_function_signature(parameters)

            def make_execute_handler(wf_name: str, params: List[tuple]):
                """Factory to create execute handler with correct closure."""
                async def execute_handler(**kwargs):
                    """Execute the workflow with validated inputs."""
                    # Look up workflow from registry
                    workflow = ombra.get_workflow(wf_name)
                    if not workflow:
                        raise HTTPException(
                            status_code=404,
                            detail=f"Workflow '{wf_name}' not found. Available workflows: {[w.name for w in ombra.get_all_workflows()]}"
                        )

                    if not workflow._runner:
                        raise HTTPException(
                            status_code=400,
                            detail=f"Workflow '{wf_name}' has no runner function defined"
                        )

                    try:
                        # Use kwargs directly (they come from FastAPI's parameter injection)
                        input_dict = kwargs

                        # Create execution
                        execution = execution_manager.create_execution(
                            workflow_name=workflow.name,
                            inputs=input_dict
                        )

                        # Start execution
                        execution.start(db=execution_manager.db)

                        # Set execution context for step tracking
                        from ..decorators import set_execution_context, clear_execution_context

                        try:
                            # Set context so @step() decorators know which execution is running
                            set_execution_context(execution.execution_id, workflow.name)

                            # Execute the runner function with inputs (always async)
                            result = await workflow._runner(**input_dict)

                            # Complete execution
                            execution.complete(result, db=execution_manager.db)

                            return _make_serializable({
                                "execution_id": execution.execution_id,
                                "status": execution.status.value,
                                "workflow_name": workflow.name,
                                "message": "Workflow executed successfully",
                                "result": result,
                                "steps_executed": [step["step_name"] for step in execution.step_executions]
                            })

                        except Exception as e:
                            execution.fail(str(e), db=execution_manager.db)
                            raise HTTPException(status_code=500, detail=str(e))

                        finally:
                            # Always clear context
                            clear_execution_context()

                    except Exception as e:
                        if isinstance(e, HTTPException):
                            raise
                        raise HTTPException(status_code=400, detail=str(e))

                return execute_handler

            # Register the endpoint with dynamic parameters
            handler = make_execute_handler(workflow_name, param_specs)

            # Build function signature dynamically
            from inspect import signature, Parameter

            params_dict = {}
            for param_name, param_type, default in param_specs:
                params_dict[param_name] = Parameter(
                    param_name,
                    Parameter.KEYWORD_ONLY,
                    default=default,
                    annotation=param_type
                )

            # Update handler signature for OpenAPI
            handler.__signature__ = signature(handler).replace(parameters=list(params_dict.values()))

            app.post(
                f"/workflows/{workflow_name}/execute",
                tags=["Workflows"],
                summary=f"Execute {workflow_name}",
                description=description or f"Execute the {workflow_name} workflow",
                response_model=None
            )(handler)

        else:
            # Original flow: Create Pydantic model for JSON body
            InputModel = create_pydantic_model_from_schema(workflow_name, parameters)

            def make_execute_handler(wf_name: str, input_model: BaseModel):
                """Factory to create execute handler with correct closure."""
                async def execute_handler(inputs: input_model = Body(...)):
                    """Execute the workflow with validated inputs."""
                    # Look up workflow from registry
                    workflow = ombra.get_workflow(wf_name)
                    if not workflow:
                        raise HTTPException(
                            status_code=404,
                            detail=f"Workflow '{wf_name}' not found. Available workflows: {[w.name for w in ombra.get_all_workflows()]}"
                        )

                    if not workflow._runner:
                        raise HTTPException(
                            status_code=400,
                            detail=f"Workflow '{wf_name}' has no runner function defined"
                        )

                    try:
                        # Convert Pydantic model to dict
                        input_dict = inputs.dict() if hasattr(inputs, 'dict') else dict(inputs)

                        # Create execution
                        execution = execution_manager.create_execution(
                            workflow_name=workflow.name,
                            inputs=input_dict
                        )

                        # Start execution
                        execution.start(db=execution_manager.db)

                        # Set execution context for step tracking
                        from ..decorators import set_execution_context, clear_execution_context

                        try:
                            # Set context so @step() decorators know which execution is running
                            set_execution_context(execution.execution_id, workflow.name)

                            # Execute the runner function with inputs (always async)
                            result = await workflow._runner(**input_dict)

                            # Complete execution
                            execution.complete(result, db=execution_manager.db)

                            return _make_serializable({
                                "execution_id": execution.execution_id,
                                "status": execution.status.value,
                                "workflow_name": workflow.name,
                                "message": "Workflow executed successfully",
                                "result": result,
                                "steps_executed": [step["step_name"] for step in execution.step_executions]
                            })

                        except Exception as e:
                            execution.fail(str(e), db=execution_manager.db)
                            raise HTTPException(status_code=500, detail=str(e))

                        finally:
                            # Always clear context
                            clear_execution_context()

                    except Exception as e:
                        if isinstance(e, HTTPException):
                            raise
                        raise HTTPException(status_code=400, detail=str(e))

                return execute_handler

            # Register the endpoint
            handler = make_execute_handler(workflow_name, InputModel)
            app.post(
                f"/workflows/{workflow_name}/execute",
                tags=["Workflows"],
                summary=f"Execute {workflow_name}",
                description=description or f"Execute the {workflow_name} workflow",
                response_model=None
            )(handler)

    # Add generic endpoints (resume, list executions, etc.)

    @app.get(
        "/workflows/{workflow_name}/executions",
        tags=["Internal"],
        summary="List executions for a workflow"
    )
    async def list_executions(
        workflow_name: str = PathParam(..., description="Name of the workflow")
    ):
        """Get all executions for a specific workflow."""
        # Verify workflow exists
        workflow = ombra.get_workflow(workflow_name)
        if not workflow:
            raise HTTPException(
                status_code=404,
                detail=f"Workflow '{workflow_name}' not found"
            )

        executions = execution_manager.get_executions_for_workflow(workflow_name)
        return {
            "workflow_name": workflow_name,
            "executions": [ex.to_dict() for ex in executions]
        }

    @app.post(
        "/workflows/{workflow_name}/resume",
        tags=["Internal"],
        summary="Resume workflow from a checkpoint (Time Travel)"
    )
    async def resume_workflow(
        workflow_name: str = PathParam(..., description="Name of the workflow"),
        step_name: str = Query(..., description="Name of the step to resume from"),
        modify_inputs: Dict[str, Any] | None = None
    ):
        """Resume workflow execution from a specific step.

        This is the time-travel feature - it loads the checkpoint for the specified step
        and re-executes from that point, creating a new execution.

        Args:
            workflow_name: Name of the workflow to resume
            step_name: Name of the step to resume from
            modify_inputs: Optional dict to override step inputs
        """
        # Look up workflow from registry
        workflow = ombra.get_workflow(workflow_name)
        if not workflow:
            raise HTTPException(
                status_code=404,
                detail=f"Workflow '{workflow_name}' not found. Available workflows: {[w.name for w in ombra.get_all_workflows()]}"
            )

        try:
            # Get the most recent execution to find original runner inputs
            executions = execution_manager.get_executions_for_workflow(workflow_name)
            if not executions:
                raise HTTPException(status_code=404, detail=f"No previous executions found for workflow '{workflow_name}'")

            # Find the most recent completed execution
            completed_executions = [
                ex for ex in executions
                if ex.step_executions and ex.status.value in ["completed", "failed"]
            ]
            if not completed_executions:
                raise HTTPException(status_code=404, detail=f"No completed executions found for workflow '{workflow_name}'")

            last_execution = sorted(
                completed_executions,
                key=lambda ex: ex.started_at,
                reverse=True
            )[0]

            # Get the original runner inputs
            runner_inputs = last_execution.inputs.get("original_runner_inputs", last_execution.inputs)
            if "resumed_from_step" in runner_inputs:
                # This was itself a time-travel execution, get the original inputs
                runner_inputs = runner_inputs.get("original_runner_inputs", {})

            # Resume the entire workflow from the target step
            # This creates a new execution internally
            result = await workflow.resume_workflow_from(step_name, runner_inputs)

            # Get the new execution that was just created
            new_execution = execution_manager.get_current_execution()
            if not new_execution:
                # Fallback: get the most recent execution
                all_executions = execution_manager.get_all_executions()
                new_execution = sorted(all_executions, key=lambda ex: ex.started_at, reverse=True)[0]

            return {
                "execution_id": new_execution.execution_id,
                "workflow_name": workflow_name,
                "status": new_execution.status.value,
                "resumed_from": step_name,
                "result": result,
                "message": f"Successfully resumed entire workflow from step '{step_name}'",
                "steps_executed": [step["step_name"] for step in new_execution.step_executions]
            }

        except ValueError as e:
            raise HTTPException(status_code=404, detail=str(e))
        except Exception as e:
            raise HTTPException(status_code=500, detail=str(e))

    @app.delete(
        "/api/executions",
        tags=["Internal"],
        summary="Delete multiple executions"
    )
    async def delete_executions(execution_ids: list[str]):
        """Delete multiple executions by their IDs.

        Args:
            execution_ids: List of execution IDs to delete

        Returns:
            Summary of deletion results
        """
        if not execution_ids:
            raise HTTPException(status_code=400, detail="No execution IDs provided")

        deleted_count = 0
        not_found = []

        for execution_id in execution_ids:
            deleted = execution_manager.delete_execution(execution_id)
            if deleted:
                deleted_count += 1
            else:
                not_found.append(execution_id)

        return {
            "deleted": deleted_count,
            "not_found": not_found,
            "message": f"Successfully deleted {deleted_count} execution(s)"
        }
