import logging
from typing import Callable, Dict, Optional

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, HTMLResponse
from prometheus_client import Counter, generate_latest
from fastapi.middleware.cors import CORSMiddleware
from .context import _set_context, _clear_context, ussd, _ctx
from .parser import parse_ussd_request
from .response import ussd_response
from .sessions import get_or_create_session, get_session_by_id, get_step, set_step
from .simulator import SIMULATOR_HTML

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

# Prometheus metrics
METRIC_REQUESTS = Counter('cardisk_requests_total', 'Total USSD requests', ['type'])

class ScreenRoute:
    def __init__(self, name: str, handler: Callable[..., str]):
        self.name = name
        self.handler = handler
        self.options: Dict[str, Callable[..., str]] = {}
        self.fallback: Optional[Callable[..., str]] = None

    def _setup_cors(self):

        self.app.add_middleware(
            CORSMiddleware,
            allow_origins=["*"],
            allow_credentials=True,
            allow_methods=["*"],
            allow_headers=["*"],
        )
    def option(self, value: str):
        def decorator(func: Callable[..., str]):
            self.options[value] = func
            return func
        return decorator

    def fallback_option(self):
        def decorator(func: Callable[..., str]):
            self.fallback = func
            return func
        return decorator

class Cardisk:
    def __init__(self):
        self.app = FastAPI()
        self.routes: Dict[str, ScreenRoute] = {}
        self._setup_handlers()

    def _setup_handlers(self):
        # simulator endpoint
        @self.app.get('/')
        def simulator():
            return HTMLResponse(content=SIMULATOR_HTML)

        # metrics endpoint
        @self.app.get('/metrics')
        def metrics():
            return generate_latest()

        # error handler
        @self.app.exception_handler(Exception)
        async def generic_exception_handler(request: Request, exc: Exception):
            logger.exception('Unhandled error: %s', exc)
            return JSONResponse({'detail': 'internal server error'}, status_code=500)

        @self.app.post("/")
        async def handle_ussd(request: Request):
            body = await request.body()
            data = parse_ussd_request(body.decode())

            msisdn = data.get("msisdn", "")
            msg = data.get("msg", "") or ""
            req_type = int(data.get("type", 1))

            METRIC_REQUESTS.labels(type=str(req_type)).inc()
            logger.info("USSD request from %s type=%s msg=%s", msisdn, req_type, msg)

            # create or find session
            session_id = get_or_create_session(msisdn, req_type)
            session = get_session_by_id(session_id)

            # set context for this request
            _set_context(msisdn, session_id, msg)

            # reset ussd proxy state
            ussd.reset()

            # determine current screen
            current = get_step(session_id)
            if req_type == 1:
                current = "/"
                set_step(session_id, "/")  # Ensure new sessions start at /

            logger.info("Current route: %s, Available routes: %s", current, list(self.routes.keys()))

            # Check if we need to handle menu navigation from previous request
            prev_session = get_session_by_id(session_id)
            prev_menu_map = prev_session.get("menu_map", {}) if prev_session else {}

            # handle menu navigation (only for continuing sessions with input)
            if msg and req_type != 1 and prev_menu_map and msg in prev_menu_map:
                # User selected a menu option, navigate to that route
                next_route = prev_menu_map[msg]
                set_step(session_id, next_route)
                current = next_route
                logger.info("Menu selection %s -> route %s", msg, next_route)

            route = self.routes.get(current)
            if not route:
                _clear_context()
                return ussd_response(f"Invalid route: {current}", end_session=True)

            # if input matches an option, call that handler
            try:
                if msg and msg in route.options:
                    result = route.options[msg]()
                elif msg and "*" in route.options:
                    # treat '*' as wildcard registration too
                    result = route.options["*"](msg)
                elif msg and route.fallback:
                    result = route.fallback()
                else:
                    # call main handler
                    result = route.handler()

                # Check if ussd.menu() or ussd.screen() was called
                response_text = ussd.get_response()
                if response_text is not None:
                    result = response_text

                # Save menu_map to session if menu was displayed
                ctx = _ctx.get()
                if ctx.get("is_menu"):
                    menu_map = ctx.get("menu_map", {})
                    session = get_session_by_id(session_id)
                    if session:
                        session["menu_map"] = menu_map

            except Exception as e:
                logger.exception('Handler error')
                _clear_context()
                return ussd_response(f"Handler error: {str(e)}", end_session=True)

            # clear context after handling
            end_now = ussd.should_end_session() or get_step(session_id) == "end"
            _clear_context()
            return ussd_response(result, end_session=end_now)

    def route(self, name: str):
        def decorator(func: Callable[..., str]):
            route = ScreenRoute(name, func)
            self.routes[name] = route
            return route
        return decorator

# single default app for simple usage
app = Cardisk()
