import inspect
import json
import os
import re
from typing import Any, Callable, Union

import openai
import yaml
from pydantic import BaseModel

from semantic_router.utils.logger import logger


def is_valid(route_config: str) -> bool:
    try:
        output_json = json.loads(route_config)
        required_keys = ["name", "utterances"]

        if isinstance(output_json, list):
            for item in output_json:
                missing_keys = [key for key in required_keys if key not in item]
                if missing_keys:
                    logger.warning(
                        f"Missing keys in route config: {', '.join(missing_keys)}"
                    )
                    return False
            return True
        else:
            missing_keys = [key for key in required_keys if key not in output_json]
            if missing_keys:
                logger.warning(
                    f"Missing keys in route config: {', '.join(missing_keys)}"
                )
                return False
            else:
                return True
    except json.JSONDecodeError as e:
        logger.error(e)
        return False


class Route(BaseModel):
    name: str
    utterances: list[str]
    description: str | None = None

    def to_dict(self):
        return self.dict()

    def to_yaml(self):
        return yaml.dump(self.dict())

    @classmethod
    def from_dict(cls, data: dict):
        return cls(**data)

    @classmethod
    async def from_dynamic_route(cls, entity: Union[BaseModel, Callable]):
        """
        Generate a dynamic Route object from a function or Pydantic model using LLM
        """
        schema = cls._get_schema(item=entity)
        dynamic_route = await cls._agenerate_dynamic_route(function_schema=schema)
        return dynamic_route

    @classmethod
    def _get_schema(cls, item: Union[BaseModel, Callable]) -> dict[str, Any]:
        if isinstance(item, BaseModel):
            signature_parts = []
            for field_name, field_model in item.__annotations__.items():
                field_info = item.__fields__[field_name]
                default_value = field_info.default

                if default_value:
                    default_repr = repr(default_value)
                    signature_part = (
                        f"{field_name}: {field_model.__name__} = {default_repr}"
                    )
                else:
                    signature_part = f"{field_name}: {field_model.__name__}"

                signature_parts.append(signature_part)
            signature = f"({', '.join(signature_parts)}) -> str"
            schema = {
                "name": item.__class__.__name__,
                "description": item.__doc__,
                "signature": signature,
            }
        else:
            schema = {
                "name": item.__name__,
                "description": str(inspect.getdoc(item)),
                "signature": str(inspect.signature(item)),
                "output": str(inspect.signature(item).return_annotation),
            }
        return schema

    @classmethod
    def _parse_route_config(cls, config: str) -> str:
        # Regular expression to match content inside <config></config>
        config_pattern = r"<config>(.*?)</config>"
        match = re.search(config_pattern, config, re.DOTALL)

        if match:
            config_content = match.group(1).strip()  # Get the matched content
            return config_content
        else:
            raise ValueError("No <config></config> tags found in the output.")

    @classmethod
    async def _agenerate_dynamic_route(cls, function_schema: dict[str, Any]):
        logger.info("Generating dynamic route...")

        prompt = f"""
        You are tasked to generate a JSON configuration based on the provided
        function schema. Please follow the template below, no other tokens allowed:

        <config>
        {{
            "name": "<function_name>",
            "utterances": [
                "<example_utterance_1>",
                "<example_utterance_2>",
                "<example_utterance_3>",
                "<example_utterance_4>",
                "<example_utterance_5>"]
        }}
        </config>

        Only include the "name" and "utterances" keys in your answer.
        The "name" should match the function name and the "utterances"
        should comprise a list of 5 example phrases that could be used to invoke
        the function.

        Input schema:
        {function_schema}
        """

        client = openai.AsyncOpenAI(
            base_url="https://openrouter.ai/api/v1",
            api_key=os.getenv("OPENROUTER_API_KEY"),
        )

        completion = await client.chat.completions.create(
            model="mistralai/mistral-7b-instruct",
            messages=[
                {
                    "role": "user",
                    "content": prompt,
                },
            ],
            temperature=0.01,
            max_tokens=200,
        )

        output = completion.choices[0].message.content
        if not output:
            raise Exception("No output generated")
        route_config = cls._parse_route_config(config=output)

        logger.info(f"Generated route config:\n{route_config}")

        if is_valid(route_config):
            return Route.from_dict(json.loads(route_config))
        raise Exception("No config generated")


class RouteConfig:
    """
    Generates a RouteConfig object from a list of Route objects
    """

    routes: list[Route] = []

    def __init__(self, routes: list[Route] = []):
        self.routes = routes

    @classmethod
    def from_file(cls, path: str):
        """Load the routes from a file in JSON or YAML format"""
        logger.info(f"Loading route config from {path}")
        _, ext = os.path.splitext(path)
        with open(path, "r") as f:
            if ext == ".json":
                routes = json.load(f)
            elif ext in [".yaml", ".yml"]:
                routes = yaml.safe_load(f)
            else:
                raise ValueError(
                    "Unsupported file type. Only .json and .yaml are supported"
                )

            route_config_str = json.dumps(routes)
            if is_valid(route_config_str):
                routes = [Route.from_dict(route) for route in routes]
                return cls(routes=routes)
            else:
                raise Exception("Invalid config JSON or YAML")

    def to_dict(self):
        return [route.to_dict() for route in self.routes]

    def to_file(self, path: str):
        """Save the routes to a file in JSON or YAML format"""
        logger.info(f"Saving route config to {path}")
        _, ext = os.path.splitext(path)
        with open(path, "w") as f:
            if ext == ".json":
                json.dump(self.to_dict(), f)
            elif ext in [".yaml", ".yml"]:
                yaml.safe_dump(self.to_dict(), f)
            else:
                raise ValueError(
                    "Unsupported file type. Only .json and .yaml are supported"
                )

    def add(self, route: Route):
        self.routes.append(route)
        logger.info(f"Added route `{route.name}`")

    def get(self, name: str):
        for route in self.routes:
            if route.name == name:
                return route
        raise Exception(f"Route `{name}` not found")

    def remove(self, name: str):
        if name not in [route.name for route in self.routes]:
            logger.error(f"Route `{name}` not found")
        else:
            self.routes = [route for route in self.routes if route.name != name]
            logger.info(f"Removed route `{name}`")