import asyncio
import re
import threading
import time
import traceback
import uuid
from dataclasses import dataclass, field
from functools import cached_property
from types import SimpleNamespace
from typing import Any

import httpx
import uvicorn
from fastapi import FastAPI, APIRouter
from loguru import logger as log
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import Response
from toomanyports import PortManager

from toomanythreads import ManagedThread

DEBUG = True


@dataclass
class AppMetadata:
    url: str | None
    rel_path: str | None
    app: Any
    name: str
    base_app: Any = None
    app_type: str = "base_app"
    is_parent_of: list = field(default_factory=list)
    is_child_of: list = field(default_factory=list)
    started_at: float = field(default_factory=time.time)
    request_count: int = 0
    error_count: int = 0

    @property
    def uptime(self) -> float:
        return time.time() - self.started_at

    @property
    def request_to_error_ratio(self):
        return self.error_count / self.request_count if self.request_count > 0 else 0

    async def request(self, path: str = "/", **kwargs) -> httpx.Response:
        """Make HTTP request to linked service"""
        if not self.url:
            raise ValueError(f"No URL for {self.name}")

        url = f"{self.url.rstrip('/')}/{path.lstrip('/')}"

        async with httpx.AsyncClient() as client:
            return await client.request(url=url, **kwargs)

    async def get(self, path: str = "/", **kwargs) -> httpx.Response:
        return await self.request(path, method="GET", **kwargs)

    async def post(self, path: str = "/", **kwargs) -> httpx.Response:
        return await self.request(path, method="POST", **kwargs)

    async def put(self, path: str = "/", **kwargs) -> httpx.Response:
        return await self.request(path, method="PUT", **kwargs)

    async def delete(self, path: str = "/", **kwargs) -> httpx.Response:
        return await self.request(path, method="DELETE", **kwargs)


class ThreadedServer(FastAPI):
    def __init__(
            self,
            host: str = "localhost",
            port: int = None,
            verbose: bool = DEBUG,
    ) -> None:
        self.verbose = verbose
        self.host = host
        if port is None: port = PortManager.random_port()
        self.port = port
        PortManager.kill(self.port, force=True)
        super().__init__(debug=self.verbose)
        self.app_metadata = AppMetadata(
            url=self.url,
            rel_path="",
            app=self,
            name=self.__class__.__name__,
            base_app=self
        )
        if self.verbose: log.success(
            f"{self}: Initialized successfully!\n  - host={self.host}\n  - port={self.port}  - verbose={self.verbose}")

        @self.middleware("http")
        async def request_log(request: Request, call_next):
            self.app_metadata.request_count = self.app_metadata.request_count + 1
            if self.verbose: log.info(f"{self}: Received request to '{request.url.path}'"
                                      f"\n  - client={request.client}"
                                      f"\n  - cookies={request.cookies}"
                                      )
            try:
                return await call_next(request)
            except Exception as e:
                tb_str = traceback.format_exc()
                log.error(f"Request failed: {e}\n{tb_str}")

                self.app_metadata.error_count += 1
                return Response(e, 500)

    def __repr__(self):
        name = f"[{self.__class__.__name__}]"
        if not name == "[ThreadedServer]":
            return name
        else:
            return f"[ThreadedServer.{str(id(self))[6:]}]"

    def link(self, app: 'ThreadedServer', name: str = None):
        if self.verbose: log.debug(f"{self}: Attempting to link an application to {self}!")
        log.debug(f"Parent URL: {self.url} (host:{self.host}, port:{self.port})")
        log.debug(f"Child URL: {app.url} (host:{app.host}, port:{app.port})")
        if not name: name = app.__class__.__name__
        parent = self
        child = app
        child_name = name

        # if parent_sessions := getattr(parent, "sessions", None):
        #     log.debug(f"{self}: Linking {parent_sessions} to {child}")
        #     setattr(child, "sessions", parent_sessions)
        # if parent_users := getattr(parent, "users", None):
        #     log.debug(f"{self}: Linking {parent_users} to {child}")
        #     setattr(child, "users", parent_users)
        if parent_session_manager := getattr(parent, "session_manager", None):
            log.debug(f"{self}: Linking {parent_session_manager} to {child}")
            setattr(child, "session_manager", parent_session_manager)

        parent.app_metadata.is_parent_of.append(child)
        if not getattr(child, "app_metadata", None):
            metadata = AppMetadata(
                url=child.url,
                rel_path=None,
                app=child,
                name=child_name,
                base_app=parent,
                app_type="linked_app",
                is_child_of=[parent]
            )
            setattr(child, "app_metadata", metadata)
        else:
            child.app_metadata.url = child.url
            child.app_metadata.rel_path = None,
            child.app_metadata.base_app = parent
            child.app_metadata.app_type = "linked_app"
            child.app_metadata.is_child_of.append(parent)

        if self.verbose: log.success(f"{self}: Successfully linked {name}!\n  - metadata={child.app_metadata}")

    def include_router(self, router: APIRouter, prefix: str = None, **kwargs):
        if self.verbose: log.debug(f"{self}: Attempting to include an APIRouter to {self}!")
        if prefix:
            name = prefix[1:].replace("/", "_")
        else:
            prefix = ""
            name = f"{router.__class__.__name__}"
            if name == "APIRouter": name = f"{name}.{str(uuid.uuid1())}"
        super().include_router(router, prefix=prefix, **kwargs)

        mounter = self
        mounted = router
        mounter.app_metadata.is_parent_of.append(mounted)
        if not getattr(mounted, "app_metadata", None):
            metadata = AppMetadata(
                url=mounter.url + prefix,
                rel_path=prefix,
                app=mounted,
                name=name,
                base_app=mounter,
                app_type="mount",
                is_child_of=[mounter]
            )
            setattr(mounted, "app_metadata", metadata)
        else:
            mounted.app_metadata.url = mounter.app_metadata.url + prefix
            mounted.app_metadata.rel_path = prefix,
            mounted.app_metadata.base_app = self
            mounted.app_metadata.is_child_of.append(mounter)

        if self.verbose: log.success(f"{self}: Successfully mounted {name}!\n  - metadata={mounted.app_metadata}")

    @cached_property
    def url(self):
        if not getattr(self, "app_metadata.url", None):
            return f"http://{self.host}:{self.port}"
        else:
            return self.app_metadata.url

    @cached_property
    def secure_url(self):
        return self.url.replace(f"http:", "https:")

    @cached_property
    def uvicorn_cfg(self) -> uvicorn.Config:
        return uvicorn.Config(
            app=self,
            host=self.host,
            port=self.port,
        )

    @cached_property
    def thread(self) -> threading.Thread:  # type: ignore
        def proc(self):
            if self.verbose: log.info(f"{self}: Launching threaded server on {self.host}:{self.port}")
            server = uvicorn.Server(config=self.uvicorn_cfg)
            server.run()

        return ManagedThread(proc, self)

    # async def forward(self, path: str, request: Request = None, **params):
    #     """Forward to an endpoint via memory using dot notation (e.g., 'auth.login')"""
    #     log.debug(f"{self}: Received forwarding request for '{path}'!")
    #     endpoints = self.endpoints
    #     index = endpoints.as_dict
    #     log.debug(index)
    #     if path in index:
    #         try:
    #             current = index.get(path)
    #             if not callable(current): raise AttributeError(f"'{path}' is not a callable endpoint")
    #             log.debug(f"{self}: Found method '{current}' at '{path}'!")
    #         except Exception as e:
    #             return HTTPException(404, e)
    #
    #     async def attempt():
    #         log.info(f"{self}: Attempting to call {current} with params:")
    #         if request: params['request'] = request
    #         for each in params.items():
    #             print(f"  - {each}={params.get(each)}")
    #         attempts = 0
    #         while attempts < 3:
    #             log.debug(f"{self}: Trying attempt '{attempts + 1}' for {current}...")
    #             try:
    #                 try:
    #                     if asyncio.iscoroutinefunction(current):
    #                         return await current(**params)
    #                     else:
    #                         return current(**params)
    #                 except TypeError as e:
    #                     if "got an unexpected keyword argument" in str(e):
    #                         pattern = r"'([^']+)'"
    #                         bad_kw = re.search(pattern, str(e))
    #                         if bad_kw:
    #                             bad_param = bad_kw.group(1)
    #                             log.warning(f"{self}: Removing bad keyword, '{bad_param}'")
    #                             del params[bad_param]
    #                         raise
    #             except Exception as e:
    #                 log.error(f"{self}: Failed attempt '{attempts + 1}'\n   - {e}")
    #                 attempts = attempts + 1
    #                 if attempts == 3: raise Exception(e)
    #
    #     return await attempt()
    #
    # @property
    # def endpoints(self):
    #     ns = SimpleNamespace()
    #     flat_dict = {}
    #
    #     for route in self.routes:
    #         if hasattr(route, 'endpoint') and hasattr(route.endpoint, '__name__'):
    #             flat_dict[route.path] = route.endpoint
    #             # Split path into parts: "/auth/login" -> ["auth", "login"]
    #             path_parts = [p for p in route.path.split('/') if p and '{' not in p]
    #
    #             # Build nested structure
    #             current = ns
    #             for part in path_parts[:-1]:  # All but last part
    #                 if not hasattr(current, part):
    #                     setattr(current, part, SimpleNamespace())
    #                 current = getattr(current, part)
    #
    #             # Set the endpoint on the final part
    #             if path_parts:
    #                 setattr(current, path_parts[-1], route.endpoint)
    #             else:
    #                 setattr(current, 'root', route.endpoint)
    #
    #     setattr(ns, "as_dict", flat_dict)
    #     return ns
