"""Tool registry — bridges skills and builtins to the agent loop."""

import asyncio
import inspect
from typing import Callable

import structlog

from fliiq.runtime.agent.todo import TODO_TOOL_DEFINITION, TodoState
from fliiq.runtime.skills.base import SkillBase

log = structlog.get_logger()

ASK_USER_TOOL_DEFINITION = {
    "name": "ask_user",
    "description": "Ask the user a clarifying question when you need more information to proceed. Use sparingly — only when the answer would materially change your approach.",
    "input_schema": {
        "type": "object",
        "properties": {
            "question": {
                "type": "string",
                "description": "The question to ask the user.",
            },
        },
        "required": ["question"],
    },
}

PLAN_COMPLETE_TOOL_DEFINITION = {
    "name": "plan_complete",
    "description": (
        "Signal that your plan is ready for user review. Provide a summary "
        "and 2-4 suggested next steps. Mark at least one option with "
        "action='execute' for the user to approve execution. The system "
        "automatically adds a free-form input option."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "summary": {
                "type": "string",
                "description": "Brief summary of the plan.",
            },
            "options": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "label": {"type": "string", "description": "Short action label."},
                        "description": {"type": "string", "description": "What happens if selected."},
                        "action": {
                            "type": "string",
                            "enum": ["execute"],
                            "description": "Set to 'execute' for options that approve the plan and begin implementation. Omit for refinement options.",
                        },
                    },
                    "required": ["label", "description"],
                },
                "minItems": 2,
                "maxItems": 4,
            },
            "recommended": {
                "type": "integer",
                "description": "1-based index of the recommended option.",
            },
        },
        "required": ["summary", "options", "recommended"],
    },
}

ASK_USER_CHOICE_TOOL_DEFINITION = {
    "name": "ask_user_choice",
    "description": "Present the user with 2-4 structured options when there are multiple reasonable approaches. Each option has a label and description. You must always indicate a recommended option. In autonomous mode, the recommended option is auto-selected without prompting.",
    "input_schema": {
        "type": "object",
        "properties": {
            "question": {
                "type": "string",
                "description": "The question or decision to present.",
            },
            "options": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "label": {
                            "type": "string",
                            "description": "Short name for this option (e.g. 'Flask', 'FastAPI').",
                        },
                        "description": {
                            "type": "string",
                            "description": "Brief explanation of this option and its tradeoffs.",
                        },
                    },
                    "required": ["label", "description"],
                },
                "minItems": 2,
                "maxItems": 4,
                "description": "The options to present (2-4 items).",
            },
            "recommended": {
                "type": "integer",
                "description": "1-based index of the recommended option. Forces you to reason about the best choice.",
            },
        },
        "required": ["question", "options", "recommended"],
    },
}


INSTALL_SKILL_TOOL_DEFINITION = {
    "name": "install_skill",
    "description": (
        "Validate and install a generated skill from a directory containing "
        "SKILL.md, fliiq.yaml, and main.py. After installation, the skill is "
        "immediately available as a tool. Use this after writing skill files "
        "with write_file."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "skill_dir": {
                "type": "string",
                "description": "Absolute path to the skill directory containing SKILL.md, fliiq.yaml, and main.py.",
            },
        },
        "required": ["skill_dir"],
    },
}

CREATE_JOB_TOOL_DEFINITION = {
    "name": "create_job",
    "description": (
        "Create a scheduled or webhook-triggered background job. The job runs "
        "your prompt through the agent on a schedule (cron/every/at) or when a "
        "webhook fires. The daemon auto-starts when a job is created."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "Job name (lowercase alphanumeric + hyphens, e.g. 'daily-summary').",
            },
            "prompt": {
                "type": "string",
                "description": "The prompt the agent will execute each time the job fires.",
            },
            "trigger_type": {
                "type": "string",
                "enum": ["cron", "at", "every", "webhook"],
                "description": "How the job is triggered.",
            },
            "schedule": {
                "type": "string",
                "description": (
                    "Schedule expression: cron ('0 9 * * *'), at (ISO datetime), "
                    "every ('30s', '5m', '1h'). Required for cron/at/every."
                ),
            },
            "source": {
                "type": "string",
                "description": "Webhook source identifier (required for webhook trigger).",
            },
            "match": {
                "type": "object",
                "description": "Key-value pairs to filter webhook payloads (AND logic, dot-notation keys).",
            },
            "skills": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Skill names to make available to this job.",
            },
            "delivery_type": {
                "type": "string",
                "enum": ["email", "sms", "telegram"],
                "description": "How to deliver job results. Agent will use send_email, send_sms, or send_telegram after completing the job.",
            },
            "delivery_to": {
                "type": "string",
                "description": "Delivery recipient: email address (for email), phone number in E.164 (for sms), or chat ID (for telegram).",
            },
        },
        "required": ["name", "prompt", "trigger_type"],
    },
}

DELETE_JOB_TOOL_DEFINITION = {
    "name": "delete_job",
    "description": "Delete a scheduled or webhook-triggered background job by name.",
    "input_schema": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "Name of the job to delete.",
            },
        },
        "required": ["name"],
    },
}

LIST_JOBS_TOOL_DEFINITION = {
    "name": "list_jobs",
    "description": "List all scheduled and webhook-triggered background jobs with their status.",
    "input_schema": {
        "type": "object",
        "properties": {},
    },
}


class ToolRegistry:
    """Registry of tools available to the agent loop.

    Manages tool definitions (for LLM API calls) and handlers (for execution).
    """

    def __init__(self):
        self._tools: dict[str, dict] = {}  # name -> {"definition": dict, "handler": callable}
        self._todo_state: TodoState | None = None
        self._ask_user_handler: Callable | None = None
        self._ask_user_choice_handler: Callable | None = None
        self._plan_complete_handler: Callable | None = None
        self._mode: str = "autonomous"

    def register(self, name: str, description: str, input_schema: dict, handler) -> None:
        """Register a tool with its definition and handler."""
        self._tools[name] = {
            "definition": {
                "name": name,
                "description": description,
                "input_schema": input_schema,
            },
            "handler": handler,
        }

    def register_skill(self, skill: SkillBase) -> None:
        """Register a SkillBase as a tool. Reads skill.schema() for the definition."""
        schema = skill.schema()

        async def _handler(params: dict) -> str:
            result = await skill.execute(params)
            return str(result)

        self._tools[skill.name] = {
            "definition": {
                "name": skill.name,
                "description": schema["description"],
                "input_schema": schema["parameters"],
            },
            "handler": _handler,
        }

    def register_builtins(
        self,
        ask_user_handler: Callable | None = None,
        ask_user_choice_handler: Callable | None = None,
        plan_complete_handler: Callable | None = None,
        mode: str = "autonomous",
        project_root=None,
    ) -> None:
        """Register built-in tools (todo, ask_user, ask_user_choice, plan_complete).

        Args:
            ask_user_handler: Callback that takes a question string and returns the user's answer.
                              Can be sync or async. If None, returns unavailable message.
            ask_user_choice_handler: Callback(question, options, recommended) -> str.
                              Can be sync or async. If None, auto-selects recommended.
            plan_complete_handler: Callback(summary, options, recommended) -> str.
                              Returns "approved", "revise: ...", "rejected", or "User chose: ...".
                              Can be sync or async. Registered only in plan mode.
            mode: Current execution mode. Used by ask_user_choice for auto-select in autonomous.
        """
        self._todo_state = TodoState()
        self._ask_user_handler = ask_user_handler
        self._ask_user_choice_handler = ask_user_choice_handler
        self._plan_complete_handler = plan_complete_handler
        self._mode = mode

        def _todo_handler(params: dict) -> str:
            return self._todo_state.handle(**params)

        self._tools["todo"] = {
            "definition": TODO_TOOL_DEFINITION,
            "handler": _todo_handler,
        }

        async def _ask_user_handler_wrapper(params: dict) -> str:
            question = params.get("question", "")
            if self._ask_user_handler:
                result = self._ask_user_handler(question)
                if inspect.isawaitable(result):
                    result = await result
                return str(result)
            return "User input not available in this execution context."

        self._tools["ask_user"] = {
            "definition": ASK_USER_TOOL_DEFINITION,
            "handler": _ask_user_handler_wrapper,
        }

        async def _ask_user_choice_handler_wrapper(params: dict) -> str:
            question = params.get("question", "")
            options = params.get("options", [])
            recommended = params.get("recommended", 1)

            # Validate
            if not options or len(options) < 2:
                return "Error: ask_user_choice requires 2-4 options."
            if recommended < 1 or recommended > len(options):
                return f"Error: recommended must be 1-{len(options)}, got {recommended}."

            rec_option = options[recommended - 1]

            # Auto-select in autonomous mode
            if self._mode == "autonomous":
                return f"Auto-selected: {rec_option['label']} — {rec_option['description']}"

            # Delegate to handler if provided
            if self._ask_user_choice_handler:
                result = self._ask_user_choice_handler(question, options, recommended)
                if inspect.isawaitable(result):
                    result = await result
                return str(result)

            # Fallback: auto-select
            return f"Auto-selected (no handler): {rec_option['label']} — {rec_option['description']}"

        self._tools["ask_user_choice"] = {
            "definition": ASK_USER_CHOICE_TOOL_DEFINITION,
            "handler": _ask_user_choice_handler_wrapper,
        }

        # plan_complete — always registered, visibility controlled by callers
        async def _plan_complete_handler_wrapper(params: dict) -> str:
            summary = params.get("summary", "")
            options = params.get("options", [])
            recommended = params.get("recommended", 1)
            if self._plan_complete_handler:
                result = self._plan_complete_handler(summary, options, recommended)
                if inspect.isawaitable(result):
                    result = await result
                return str(result)
            return "Plan complete. No handler available — user cannot review."

        self._tools["plan_complete"] = {
            "definition": PLAN_COMPLETE_TOOL_DEFINITION,
            "handler": _plan_complete_handler_wrapper,
        }

        # install_skill — only in non-plan modes (needs write_file to exist)
        if mode != "plan":
            _registry_ref = self

            async def _install_skill_handler(params: dict) -> str:
                from pathlib import Path

                from fliiq.runtime.skills.installer import (
                    SkillInstallError,
                    install_skill,
                )

                skill_dir = Path(params.get("skill_dir", ""))
                try:
                    return install_skill(skill_dir, _registry_ref)
                except SkillInstallError as e:
                    return f"Skill installation failed: {e}"

            self._tools["install_skill"] = {
                "definition": INSTALL_SKILL_TOOL_DEFINITION,
                "handler": _install_skill_handler,
            }

            # Job management tools — only when project_root is known
            if project_root is not None:
                from fliiq.runtime.scheduler.job_crud import (
                    create_job_from_params,
                    delete_job_by_name,
                    list_jobs_summary,
                )

                _pr = project_root

                async def _create_job_handler(params: dict) -> str:
                    return create_job_from_params(params, _pr)

                async def _delete_job_handler(params: dict) -> str:
                    return delete_job_by_name(params.get("name", ""), _pr)

                async def _list_jobs_handler(params: dict) -> str:
                    return list_jobs_summary(_pr)

                self._tools["create_job"] = {
                    "definition": CREATE_JOB_TOOL_DEFINITION,
                    "handler": _create_job_handler,
                }
                self._tools["delete_job"] = {
                    "definition": DELETE_JOB_TOOL_DEFINITION,
                    "handler": _delete_job_handler,
                }
                self._tools["list_jobs"] = {
                    "definition": LIST_JOBS_TOOL_DEFINITION,
                    "handler": _list_jobs_handler,
                }

    def get_tool_definitions(self) -> list[dict]:
        """Return tool definitions formatted for the LLM API call."""
        return [entry["definition"] for entry in self._tools.values()]

    async def execute(self, name: str, tool_input: dict) -> str:
        """Execute a tool by name. Returns result string. Catches errors."""
        entry = self._tools.get(name)
        if not entry:
            return f"Error: unknown tool '{name}'."

        handler = entry["handler"]
        try:
            import asyncio
            import inspect

            if inspect.iscoroutinefunction(handler):
                result = await handler(tool_input)
            else:
                result = handler(tool_input)
            return str(result)
        except Exception as e:
            log.warning("tool_execution_error", tool=name, error=str(e))
            return f"Error executing {name}: {e}"
