"""
Litestar API generator for halfORM projects.

Scans all halfORM Relation subclasses in the project for methods decorated
with ``@tools.api_*`` and produces ``api/main.py``.
"""

import importlib
import inspect
import os
import re
import shutil
import sys
from pathlib import Path
from typing import Iterable, Tuple, Type

from half_orm.relation import Relation


# ---------------------------------------------------------------------------
# Templates for the generated main.py
# ---------------------------------------------------------------------------

_HEADER_TEMPLATE = """\
# This file is generated by `half_orm litestar generate`. Do not edit by hand.
from typing import Union, Any
import datetime
import uuid
import os
import sys
import typing
from typing import Optional

cur_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, cur_dir)
par_dir = os.path.join(cur_dir, os.path.pardir)
sys.path.insert(0, par_dir)

from litestar import Request, Litestar, get, post, patch, put, delete, Response, MediaType
from litestar.exceptions import HTTPException
from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR
from litestar.logging import LoggingConfig
from dataclasses import dataclass, field

from {module} import ho_dataclasses
from api import guards

try:
    from api.custom.routes import routes
except ImportError:
    routes = []

try:
    from api.custom.middlewares import middlewares
except ImportError:
    middlewares = []

try:
    from api.custom.middlewares.authorization import Authorization
    _auth_middleware = [Authorization]
except ImportError:
    _auth_middleware = []

logging_config = LoggingConfig(
    loggers={{
        "app": {{
            "level": "ERROR",
            "handlers": ["queue_listener"],
        }}
    }}
)

application = Litestar(
    route_handlers={route_handlers},
    middleware=_auth_middleware + middlewares,
    logging_config=logging_config,
    debug=True,
)

"""

_IMPORT_TEMPLATE = "\nfrom {schema} import {module_name} as {module_alias}\n"

_GET_TEMPLATE = """
@get({litestar_args})
async def {full_name}({query_params}) -> typing.List[ho_dataclasses.{dc_name}]:
    return await {module_alias}.{class_name}().{name}({params})

application.register({full_name})
"""

_POST_TEMPLATE = """
@post({litestar_args})
async def {full_name}({query_params}) -> ho_dataclasses.{dc_name}:
    return await {module_alias}.{class_name}().{name}({params})

application.register({full_name})
"""

_PATCH_TEMPLATE = """
@patch({litestar_args})
async def {full_name}(request: "Request", data: ho_dataclasses.{dc_name}) -> ho_dataclasses.{dc_name}:
    return await {module_alias}.{class_name}().{name}({params})

application.register({full_name})
"""

_PUT_TEMPLATE = """
@put({litestar_args})
async def {full_name}(request: "Request", {path_params}) -> ho_dataclasses.{dc_name}:
    return await {module_alias}.{class_name}().{name}({params})

application.register({full_name})
"""

_DELETE_TEMPLATE = """
@delete({litestar_args})
async def {full_name}(request: "Request", {path_params}) -> None:
    return await {module_alias}.{class_name}().{name}({params})

application.register({full_name})
"""

_DIRECT_API_TEMPLATE = """
from {module_str} import {function} as {function_alias}

application.register({function_alias})
"""

_HTTP_TEMPLATES = {
    'GET':    _GET_TEMPLATE,
    'POST':   _POST_TEMPLATE,
    'PATCH':  _PATCH_TEMPLATE,
    'PUT':    _PUT_TEMPLATE,
    'DELETE': _DELETE_TEMPLATE,
}

_RE_PATH_VAR = re.compile(r'\{([^:]*):([^\}]*)\}')

# Internal Litestar decorator parameters that should not be forwarded
_LITESTAR_INTERNAL_PARAMS = {
    'operation_class', 'operation_id', 'tags', 'summary',
    'response_description', 'responses', 'deprecated',
    'cache', 'etag', 'return_dto', 'dto', 'type_encoders',
    'sync_to_thread', 'opt', 'signature_namespace',
    'include_in_schema', 'security', 'middleware',
}

_LITESTAR_DEFAULT_VALUES = {
    'cache': False,
    'deprecated': False,
    'sync_to_thread': True,
    'include_in_schema': True,
}


# ---------------------------------------------------------------------------
# Scaffolding helpers
# ---------------------------------------------------------------------------

_SCAFFOLDING_DIR = Path(__file__).parent / 'scaffolding'


def _scaffold_api_dir(api_dir: Path) -> None:
    """Create missing api/ scaffolding files. Never overwrites existing files."""
    files = {
        api_dir / 'guards.py':
            _SCAFFOLDING_DIR / 'guards.py',
        api_dir / '__init__.py':
            _SCAFFOLDING_DIR / 'api_init.py',
        api_dir / 'custom' / 'routes.py':
            _SCAFFOLDING_DIR / 'custom_routes.py',
        api_dir / 'custom' / '__init__.py':
            _SCAFFOLDING_DIR / 'custom_init.py',
        api_dir / 'custom' / 'middlewares' / '__init__.py':
            _SCAFFOLDING_DIR / 'custom_middlewares_init.py',
        api_dir / 'custom' / 'middlewares' / 'authorization.py':
            _SCAFFOLDING_DIR / 'custom_authorization.py',
    }
    for dest, src in files.items():
        if not dest.exists():
            dest.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy(src, dest)
            print(f'  created  {dest}')
        else:
            print(f'  exists   {dest}')


# ---------------------------------------------------------------------------
# Core generator
# ---------------------------------------------------------------------------

class GenApi:
    """
    Generate ``api/main.py`` from halfORM Relation classes decorated with
    ``@tools.api_*``.

    Parameters
    ----------
    repo:
        A ``half_orm_dev.repo.Repo`` instance (provides project name, base
        directory and the list of relation classes).  When *None*, the caller
        must supply *relation_classes* and *base_dir* / *module_name*.
    relation_classes:
        An iterable of ``(RelationClass, relation_type)`` pairs, used when
        *repo* is *None*.
    module_name:
        The top-level Python package name of the halfORM model (e.g. ``"odj"``).
        Inferred from *repo* when available.
    base_dir:
        Root directory of the project (``api/`` will be created inside it).
        Inferred from *repo* when available.
    """

    def __init__(
        self,
        repo=None,
        *,
        relation_classes: Iterable[Tuple[Type[Relation], str]] | None = None,
        module_name: str | None = None,
        base_dir: str | None = None,
    ):
        if repo is not None:
            self._module_name = repo.name
            self._base_dir = Path(repo.base_dir)
            self._classes = repo.model.classes()
        else:
            if relation_classes is None or module_name is None or base_dir is None:
                raise ValueError(
                    "Provide either a repo or (relation_classes, module_name, base_dir)."
                )
            self._module_name = module_name
            self._base_dir = Path(base_dir)
            self._classes = relation_classes

        self._api_dir = self._base_dir / 'api'
        self._generate()

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _path_params(self, api_path: str) -> str:
        parts = [f'{var}: Any' for var, _ in re.findall(_RE_PATH_VAR, api_path)]
        return ', '.join(parts)

    def _query_params(self, signature: inspect.Signature):
        params_decl = []
        params_call = []
        for name, param in signature.parameters.items():
            if name == 'self':
                continue
            params_call.append(name)
            decl = name
            if param.annotation is not inspect.Parameter.empty:
                decl = f'{decl}: "{param.annotation}"'
            if param.default is not inspect.Parameter.empty:
                decl = f'{decl}={param.default!r}'
            params_decl.append(decl)
        return ', '.join(params_decl), ', '.join(params_call)

    def _format_litestar_args(
        self, litestar_params: dict, guards_list: list[str], method_doc: str
    ) -> str:
        args = []

        # Path is always first positional argument
        if 'path' in litestar_params:
            full_path = f"/{self._module_name}{litestar_params['path']}"
            args.append(f'"{full_path}"')

        kwargs = []

        if guards_list:
            guards_str = ', '.join(f'guards.{g}' for g in guards_list)
            kwargs.append(f'guards=[{guards_str}]')

        if litestar_params.get('name'):
            kwargs.append(f'name="{litestar_params["name"]}"')

        # Build description from docstring + guards info
        desc_parts = [p for p in [method_doc, f"Guards: {', '.join(guards_list)}" if guards_list else ''] if p]
        if desc_parts:
            kwargs.append(f'description="""{chr(10).join(desc_parts)}"""')
        elif litestar_params.get('description'):
            kwargs.append(f'description="""{litestar_params["description"]}"""')

        for key, value in litestar_params.items():
            if key in ('path', 'guards', 'name', 'description'):
                continue
            if key in _LITESTAR_INTERNAL_PARAMS:
                continue
            if key in _LITESTAR_DEFAULT_VALUES and value == _LITESTAR_DEFAULT_VALUES[key]:
                continue
            # Skip complex/internal objects
            if hasattr(value, '__class__'):
                cls = value.__class__
                if 'Empty' in str(cls) or 'enum' in str(cls).lower():
                    continue
                if cls.__module__ not in ('builtins', '__builtin__'):
                    continue
            if isinstance(value, str):
                kwargs.append(f'{key}="{value}"')
            elif isinstance(value, bool):
                kwargs.append(f'{key}={value}')
            elif isinstance(value, (int, float)):
                kwargs.append(f'{key}={value}')
            elif value is not None:
                kwargs.append(f'{key}={value!r}')

        return ', '.join(args + kwargs)

    @staticmethod
    def _extract_guards(litestar_params: dict) -> list[str]:
        guards = litestar_params.get('guards') or []
        result = []
        for g in guards:
            if hasattr(g, '__name__'):
                result.append(g.__name__)
            elif isinstance(g, str):
                result.append(g)
            else:
                result.append(str(g))
        return result

    # ------------------------------------------------------------------
    # Main generation
    # ------------------------------------------------------------------

    def _generate(self) -> None:
        os.environ.setdefault('API_GEN_MODE', '1')

        # Collect all route handlers (for the Litestar constructor)
        route_handlers: list[str] = []
        blocks: list[str] = []

        for relation, _relation_type in self._classes:
            module_str   = relation.__module__
            schema       = '.'.join(module_str.split('.')[:-1])
            module_name  = module_str.split('.')[-1]
            module_alias = module_str.replace('.', '_')

            schema_cap   = ''.join(p.capitalize() for p in relation._schemaname.split('.'))
            full_class   = f'{relation._dbname.capitalize()}{schema_cap}{relation.__name__}'

            api_methods = [
                (name, method)
                for name, method in inspect.getmembers(relation, predicate=inspect.isfunction)
                if getattr(method, 'is_api_route', False) and name in relation.__dict__
            ]

            if not api_methods:
                # Still check for module-level API list
                mod = importlib.import_module(module_str)
                if hasattr(mod, 'API'):
                    for func in mod.API:
                        alias = str(func).replace('.', '_')
                        fname = str(func).split('.')[-1]
                        blocks.append(_DIRECT_API_TEMPLATE.format(
                            module_str=module_str,
                            function=fname,
                            function_alias=alias,
                        ))
                        route_handlers.append(alias)
                continue

            # Import block for this module
            blocks.append(_IMPORT_TEMPLATE.format(
                schema=schema,
                module_name=module_name,
                module_alias=module_alias,
            ))

            for name, method in api_methods:
                litestar_params = method.litestar_params
                metadata        = method.metadata
                verb            = method.http_method
                guards_list     = self._extract_guards(litestar_params)
                sig             = metadata.get('signature', inspect.signature(method))
                query_params, call_params = self._query_params(sig)
                litestar_args = self._format_litestar_args(
                    litestar_params, guards_list, metadata.get('documentation', '')
                )

                full_name = f'{module_alias}_{name}'
                template  = _HTTP_TEMPLATES.get(verb)
                if template is None:
                    print(f'  warning: no template for HTTP method {verb} ({module_str}.{name})')
                    continue

                print(f'  {verb:6s} {litestar_params.get("path", "")} → {full_name}')

                blocks.append(template.format(
                    full_name    = full_name,
                    litestar_args= litestar_args,
                    dc_name      = relation._ho_dataclass_name(),
                    module_alias = module_alias,
                    class_name   = relation.__name__,
                    path_params  = self._path_params(litestar_params.get('path', '')),
                    query_params = query_params,
                    params       = call_params,
                    name         = name,
                ))
                route_handlers.append(full_name)

            # Module-level API list
            mod = importlib.import_module(module_str)
            if hasattr(mod, 'API'):
                for func in mod.API:
                    alias = str(func).replace('.', '_')
                    fname = str(func).split('.')[-1]
                    blocks.append(_DIRECT_API_TEMPLATE.format(
                        module_str=module_str,
                        function=fname,
                        function_alias=alias,
                    ))
                    route_handlers.append(alias)

        # Scaffold missing api/ files
        print(f'\nScaffolding {self._api_dir} ...')
        _scaffold_api_dir(self._api_dir)

        # Write main.py
        route_handlers_str = ', '.join(route_handlers)
        output = _HEADER_TEMPLATE.format(
            module=self._module_name,
            route_handlers=f'[{route_handlers_str}]',
        ) + ''.join(blocks)

        main_py = self._api_dir / 'main.py'
        main_py.write_text(output, encoding='utf-8')
        print(f'\nGenerated {main_py}')