from __future__ import annotations

import asyncio
import logging
import os
from pathlib import Path
from datetime import datetime, timezone
from typing import Any
from urllib.parse import parse_qsl

import httpx
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, Response
from fastapi.staticfiles import StaticFiles

from services.shared.models import HealthResponse

app = FastAPI(title="api-gateway", version="1.0.0")
logger = logging.getLogger("api-gateway.proxy")
_ERROR_ALERT_SENT_AT: dict[str, float] = {}
_ERROR_ALERT_MAX_MESSAGE_LENGTH = 3800
_ERROR_ALERT_HEADER_PREVIEW_LIMIT = 120
_ERROR_ALERT_BODY_PREVIEW_LIMIT = 1200


def _env_flag(name: str, default: bool = False) -> bool:
    value = os.getenv(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


def _env_int(name: str, default: int) -> int:
    value = os.getenv(name)
    if value is None:
        return default
    try:
        parsed = int(value)
    except ValueError:
        return default
    return max(1, parsed)


def _error_alert_enabled() -> bool:
    return _env_flag("ERROR_TELEGRAM_ALERTS", False)


def _error_alert_bot_token() -> str:
    return os.getenv("ERROR_TELEGRAM_ALERT_BOT_TOKEN", "").strip()


def _normalize_telegram_chat_id(raw_id: str) -> str:
    value = raw_id.strip()
    if not value:
        return value

    lowered = value.lower()
    for prefix in ("https://t.me/", "http://t.me/", "t.me/"):
        if lowered.startswith(prefix):
            value = value[len(prefix):].strip()
            break

    if not value.startswith(("@", "-100")) and not value.isdigit():
        value = f"@{value}"
    return value


def _error_alert_chat_ids() -> list[str]:
    raw = os.getenv("ERROR_TELEGRAM_ALERT_CHAT_IDS", "").strip()
    if not raw:
        raw = os.getenv("ERROR_TELEGRAM_ALERT_CHANNEL_ID", "").strip()
        if not raw:
            raw = os.getenv("ERROR_TELEGRAM_ALERT_CHANNEL", "").strip()
    normalized = [_normalize_telegram_chat_id(item) for item in raw.split(",")]
    return [item for item in normalized if item]


def _error_alert_min_status() -> int:
    return max(1, _env_int("ERROR_TELEGRAM_ALERT_MIN_STATUS", 500))


def _error_alert_cooldown_seconds() -> int:
    return _env_int("ERROR_TELEGRAM_ALERT_COOLDOWN_SECONDS", 30)


def _error_alert_ignored_paths() -> tuple[str, ...]:
    raw = os.getenv("ERROR_TELEGRAM_ALERT_IGNORE_PATHS", "/health").strip()
    paths = tuple(item.strip() for item in raw.split(",") if item.strip())
    return paths if paths else ("/health",)


def _error_alert_service_name() -> str:
    return os.getenv("ERROR_TELEGRAM_ALERT_SERVICE", "api-gateway").strip() or "api-gateway"


def _error_alert_bot_api_base() -> str:
    return os.getenv("ERROR_TELEGRAM_ALERT_BOT_API_BASE", "https://api.telegram.org").rstrip("/")


def _error_alert_should_alert_path(path: str) -> bool:
    ignore = _error_alert_ignored_paths()
    return not any(path == item or path.startswith(f"{item}/") for item in ignore)


def _error_alert_key(service: str, status: int, path: str, method: str) -> str:
    return f"{service}:{status}:{method}:{path}"


def _error_alert_mask_value(value: str) -> str:
    text = value.strip()
    if len(text) <= 6:
        return "<hidden>"
    return f"{text[:3]}...{text[-2:]}"


def _error_alert_headers_preview(headers: Any) -> list[str]:
    sensitive = {
        "authorization",
        "x-telegram-bot-api-secret-token",
        "x-whatsapp-webhook-secret",
        "x-hub-signature-256",
        "cookie",
        "set-cookie",
        "proxy-authorization",
        "x-api-key",
        "api-key",
        "x-auth-token",
        "x-service-token",
    }
    result: list[str] = []
    for key, value in headers.items():
        key_lower = key.lower()
        if key_lower in sensitive:
            value = _error_alert_mask_value(value)
        result.append(f"{key}: {value}")

    return result[:_ERROR_ALERT_HEADER_PREVIEW_LIMIT]


def _error_alert_mask_query(query: str) -> str:
    if not query:
        return "-"

    sensitive = {
        "token",
        "access_token",
        "refresh_token",
        "authorization",
        "password",
        "secret",
        "api_key",
        "apikey",
        "x_api_key",
        "x-telegram-bot-api-secret-token",
        "x-whatsapp-webhook-secret",
        "signature",
    }
    parsed = parse_qsl(query, keep_blank_values=True)
    if not parsed:
        return query

    values = []
    for key, value in parsed:
        key_lower = key.lower()
        if key_lower in sensitive:
            values.append(f"{key}=<hidden>")
        else:
            values.append(f"{key}={value}")
    return "&".join(values)


def _error_alert_request_context(request: Request) -> dict[str, Any]:
    query = request.url.query
    client = request.client
    client_addr = f"{client.host}:{client.port}" if client else "unknown"

    return {
        "client": client_addr,
        "http_version": request.scope.get("http_version", "-"),
        "query": _error_alert_mask_query(query),
        "user_agent": request.headers.get("user-agent", "-"),
        "referer": request.headers.get("referer", "-"),
        "host": request.headers.get("host", "-"),
        "request_id": request.headers.get("x-request-id")
        or request.headers.get("x-correlation-id")
        or request.headers.get("x-amzn-trace-id")
        or "-",
        "x_user": request.headers.get("x-user", "-"),
        "x_role": request.headers.get("x-role", "-"),
        "x_tenant_id": request.headers.get("x-tenant-id", "-"),
        "x_forwarded_for": request.headers.get("x-forwarded-for", "-"),
        "headers_count": len(request.headers),
        "headers": _error_alert_headers_preview(request.headers),
    }


def _error_alert_message_payload(
    *,
    service: str,
    method: str,
    path: str,
    status: int,
    detail: str,
    request: Request,
    extra: dict[str, Any] | None = None,
) -> str:
    timestamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    request_context = _error_alert_request_context(request)

    lines = [
        f"🚨 [{service}] {status}",
        f"time: {timestamp}",
        f"method: {method}",
        f"path: {path}",
        f"service: {service}",
        f"client: {request_context['client']}",
        f"http_version: {request_context['http_version']}",
        f"query: {request_context['query']}",
        f"request_id: {request_context['request_id']}",
        f"user_agent: {request_context['user_agent']}",
        f"referer: {request_context['referer']}",
        f"host: {request_context['host']}",
        f"x_user: {request_context['x_user']}",
        f"x_role: {request_context['x_role']}",
        f"x_tenant_id: {request_context['x_tenant_id']}",
        f"x_forwarded_for: {request_context['x_forwarded_for']}",
        "",
        "headers:",
    ]

    lines.extend(f"  {item}" for item in request_context["headers"])

    if extra:
        lines.extend(("", "extra:"))
        for key, value in extra.items():
            lines.append(f"  {key}: {value}")

    lines.append("")
    lines.append(f"detail: {detail}")

    full_message = "\n".join(lines)
    if len(full_message) <= _ERROR_ALERT_MAX_MESSAGE_LENGTH:
        return full_message

    cutoff = _ERROR_ALERT_MAX_MESSAGE_LENGTH - 80
    if cutoff < 100:
        return full_message[:_ERROR_ALERT_MAX_MESSAGE_LENGTH]

    return f"{full_message[:cutoff]}\n... (truncated)"


def _error_alert_body_preview(payload: bytes | None) -> str:
    if payload is None:
        return "-"
    if len(payload) > _ERROR_ALERT_BODY_PREVIEW_LIMIT:
        payload = payload[:_ERROR_ALERT_BODY_PREVIEW_LIMIT]
        truncated = True
    else:
        truncated = False

    try:
        text = payload.decode("utf-8")
    except UnicodeDecodeError:
        text = payload.decode("utf-8", errors="replace")

    if truncated:
        return f"{text}\n... (truncated body preview)"
    return text


def _error_alert_is_throttled(key: str) -> bool:
    now = datetime.now(tz=timezone.utc).timestamp()
    cooldown = _error_alert_cooldown_seconds()
    previous = _ERROR_ALERT_SENT_AT.get(key, 0.0)
    if now < previous:
        return True
    _ERROR_ALERT_SENT_AT[key] = now + cooldown
    return False


async def _send_telegram_alert(message: str) -> None:
    chat_ids = _error_alert_chat_ids()
    bot_token = _error_alert_bot_token()
    if not bot_token or not chat_ids:
        return

    url = f"{_error_alert_bot_api_base()}/bot{bot_token}/sendMessage"
    payload = {"text": message}
    async with httpx.AsyncClient(timeout=3.0) as client:
        for chat_id in chat_ids:
            payload["chat_id"] = chat_id
            try:
                response = await client.post(url, json=payload)
            except httpx.RequestError:
                logger.exception("Unable to send Telegram error notification")
                return
            if response.status_code >= 300:
                logger.warning(
                    "Telegram error notification failed: status=%s body=%s",
                    response.status_code,
                    response.text,
                )


def _queue_telegram_error_alert(
    service: str,
    method: str,
    path: str,
    status: int,
    detail: str,
    request: Request | None = None,
    extra: dict[str, Any] | None = None,
) -> None:
    if not _error_alert_enabled():
        return
    if not _error_alert_should_alert_path(path):
        return
    if not _error_alert_bot_token() or not _error_alert_chat_ids():
        return

    if _error_alert_is_throttled(_error_alert_key(service, status, path, method)):
        return

    if request is None:
        return

    message = _error_alert_message_payload(
        service=service,
        method=method,
        path=path,
        status=status,
        detail=detail,
        request=request,
        extra=extra,
    )

    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        logger.warning("No running loop to queue telegram error notification")
        return
    loop.create_task(_send_telegram_alert(message))

_LEGAL_HTML = {
    "privacy": """<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>KonturAI Privacy Policy</title>
    <style>
      body { font-family: Arial, sans-serif; margin: 0; background: #f5f7fb; color: #172033; }
      main { max-width: 760px; margin: 48px auto; background: #ffffff; padding: 40px; border-radius: 18px; box-shadow: 0 10px 40px rgba(23, 32, 51, 0.08); }
      h1, h2 { color: #0d3b2e; }
      p, li { line-height: 1.6; }
      a { color: #0b6bcb; }
    </style>
  </head>
  <body>
    <main>
      <h1>KonturAI Privacy Policy</h1>
      <p>KonturAI processes customer communication data to route conversations, assist operators, and provide AI-supported support workflows across channels including WhatsApp.</p>
      <h2>Data We Process</h2>
      <ul>
        <li>Profile data shared by the messaging platform, including display name and phone number.</li>
        <li>Message content and delivery metadata needed to operate support conversations.</li>
        <li>Operational data used for routing, analytics, and service quality monitoring.</li>
      </ul>
      <h2>How We Use Data</h2>
      <ul>
        <li>Deliver inbound and outbound customer support messages.</li>
        <li>Route conversations to AI or human operators.</li>
        <li>Generate summaries, audit trails, and service performance reports.</li>
      </ul>
      <h2>Contact</h2>
      <p>For privacy-related requests, contact <a href="mailto:ernurreen@gmail.com">ernurreen@gmail.com</a>.</p>
    </main>
  </body>
</html>
""",
    "terms": """<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>KonturAI Terms of Service</title>
    <style>
      body { font-family: Arial, sans-serif; margin: 0; background: #f5f7fb; color: #172033; }
      main { max-width: 760px; margin: 48px auto; background: #ffffff; padding: 40px; border-radius: 18px; box-shadow: 0 10px 40px rgba(23, 32, 51, 0.08); }
      h1, h2 { color: #0d3b2e; }
      p, li { line-height: 1.6; }
    </style>
  </head>
  <body>
    <main>
      <h1>KonturAI Terms of Service</h1>
      <p>KonturAI provides customer communication tooling for internal support operations, including operator workspaces, AI-assisted workflows, and messaging integrations.</p>
      <h2>Use of Service</h2>
      <ul>
        <li>The service is intended for authorized business use only.</li>
        <li>Users are responsible for ensuring their communications comply with applicable law and platform policies.</li>
        <li>KonturAI may log operational events to maintain service reliability and auditability.</li>
      </ul>
      <h2>Availability</h2>
      <p>The service is provided on a best-effort basis and may be updated, improved, or temporarily interrupted for maintenance.</p>
      <h2>Contact</h2>
      <p>Questions about these terms can be sent to ernurreen@gmail.com.</p>
    </main>
  </body>
</html>
""",
    "data-deletion": """<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>KonturAI Data Deletion</title>
    <style>
      body { font-family: Arial, sans-serif; margin: 0; background: #f5f7fb; color: #172033; }
      main { max-width: 760px; margin: 48px auto; background: #ffffff; padding: 40px; border-radius: 18px; box-shadow: 0 10px 40px rgba(23, 32, 51, 0.08); }
      h1, h2 { color: #0d3b2e; }
      p, li { line-height: 1.6; }
      code { background: #eef3fb; padding: 2px 6px; border-radius: 6px; }
    </style>
  </head>
  <body>
    <main>
      <h1>KonturAI Data Deletion Instructions</h1>
      <p>To request deletion of personal data processed through KonturAI, send an email to <a href="mailto:ernurreen@gmail.com">ernurreen@gmail.com</a> with the subject line <code>Data Deletion Request</code>.</p>
      <h2>Please include</h2>
      <ul>
        <li>Your phone number or account identifier used in the conversation.</li>
        <li>The approximate date of the interaction.</li>
        <li>Any details needed to identify the record you want removed.</li>
      </ul>
      <p>Requests are reviewed and processed within a reasonable timeframe subject to legal, security, and audit retention obligations.</p>
    </main>
  </body>
</html>
""",
}

_ROOT = Path(__file__).resolve().parents[1]
_LOGIN_UI_DIR = _ROOT / "ui" / "login"
_OPERATOR_UI_DIR = _ROOT / "ui" / "operator"
_SUPERVISOR_UI_DIR = _ROOT / "ui" / "supervisor"
_ANALYST_UI_DIR = _ROOT / "ui" / "analyst"
_ADMIN_UI_DIR = _ROOT / "ui" / "admin"
_SALES_UI_DIR = _ROOT / "ui" / "sales"
_IVR_PREVIEW_AUDIO_DIR = Path(
    os.getenv("IVR_PREVIEW_AUDIO_DIR", str(_ROOT / ".data_local" / "generated_ivr_yandex"))
).expanduser()
if _LOGIN_UI_DIR.exists():
    app.mount("/login/assets", StaticFiles(directory=str(_LOGIN_UI_DIR)), name="login_assets")
if _OPERATOR_UI_DIR.exists():
    app.mount("/operator/assets", StaticFiles(directory=str(_OPERATOR_UI_DIR)), name="operator_assets")
if _SUPERVISOR_UI_DIR.exists():
    app.mount("/supervisor/assets", StaticFiles(directory=str(_SUPERVISOR_UI_DIR)), name="supervisor_assets")
if _ANALYST_UI_DIR.exists():
    app.mount("/analyst/assets", StaticFiles(directory=str(_ANALYST_UI_DIR)), name="analyst_assets")
if _ADMIN_UI_DIR.exists():
    app.mount("/admin/assets", StaticFiles(directory=str(_ADMIN_UI_DIR)), name="admin_assets")
if _SALES_UI_DIR.exists():
    app.mount("/sales/assets", StaticFiles(directory=str(_SALES_UI_DIR)), name="sales_assets")
if _IVR_PREVIEW_AUDIO_DIR.exists():
    app.mount("/_preview_audio", StaticFiles(directory=str(_IVR_PREVIEW_AUDIO_DIR)), name="ivr_preview_audio")

SERVICE_URLS = {
    "auth": os.getenv("AUTH_SERVICE_URL", "http://localhost:8001"),
    "audit": os.getenv("AUDIT_SERVICE_URL", "http://localhost:8002"),
    "customer": os.getenv("CUSTOMER_SERVICE_URL", "http://localhost:8003"),
    "interaction": os.getenv("INTERACTION_SERVICE_URL", "http://localhost:8004"),
    "routing": os.getenv("ROUTING_SERVICE_URL", "http://localhost:8005"),
    "voice": os.getenv("VOICE_ADAPTER_SERVICE_URL", "http://localhost:8006"),
    "recording": os.getenv("RECORDING_SERVICE_URL", "http://localhost:8013"),
    "ivr": os.getenv("IVR_SERVICE_URL", "http://localhost:8014"),
    "event-bus": os.getenv("EVENT_BUS_SERVICE_URL", "http://localhost:8015"),
    "asterisk-bridge": os.getenv("ASTERISK_BRIDGE_SERVICE_URL", "http://localhost:8016"),
    "telegram": os.getenv("TELEGRAM_ADAPTER_SERVICE_URL", "http://localhost:8007"),
    "whatsapp": os.getenv("WHATSAPP_ADAPTER_SERVICE_URL", "http://localhost:8019"),
    "ai": os.getenv("AI_ORCHESTRATOR_SERVICE_URL", "http://localhost:8017"),
    "ai-voice-runtime": os.getenv("AI_VOICE_RUNTIME_SERVICE_URL", "http://localhost:8018"),
    "webchat": os.getenv("WEBCHAT_ADAPTER_SERVICE_URL", "http://localhost:8011"),
    "email": os.getenv("EMAIL_ADAPTER_SERVICE_URL", "http://localhost:8012"),
    "kb": os.getenv("KB_SERVICE_URL", "http://localhost:8008"),
    "reporting": os.getenv("REPORTING_SERVICE_URL", "http://localhost:8009"),
    "supervisor": os.getenv("SUPERVISOR_SERVICE_URL", "http://localhost:8010"),
    "sales": os.getenv("SALES_SERVICE_URL", "http://localhost:8020"),
}
SERVICE_PATH_PREFIXES = {
    "sales": os.getenv("SALES_SERVICE_PATH_PREFIX", "/api/v1"),
}
SERVICE_PATH_PREFIX_EXEMPTIONS = {
    "sales": ("api/v1", "internal"),
}


def _resolve_service_path(service: str, path: str) -> str:
    normalized = path.lstrip("/")
    prefix = SERVICE_PATH_PREFIXES.get(service, "").strip("/")
    if not prefix:
        return normalized

    exemptions = SERVICE_PATH_PREFIX_EXEMPTIONS.get(service, ())
    if any(normalized == item or normalized.startswith(f"{item}/") for item in exemptions):
        return normalized
    return f"{prefix}/{normalized}" if normalized else prefix


@app.middleware("http")
async def error_notification_middleware(request: Request, call_next):
    try:
        response = await call_next(request)
    except Exception as exc:
        status_code = 500
        if isinstance(exc, HTTPException):
            status_code = exc.status_code
        if status_code >= _error_alert_min_status():
            _queue_telegram_error_alert(
                service=_error_alert_service_name(),
                method=request.method,
                path=str(request.url.path),
                status=status_code,
                detail=f"{type(exc).__name__}: {exc}",
                request=request,
            )
        raise

    if response.status_code >= _error_alert_min_status():
        _queue_telegram_error_alert(
            service=_error_alert_service_name(),
            method=request.method,
            path=str(request.url.path),
            status=response.status_code,
            detail=f"HTTP {response.status_code} from {request.method} {request.url.path}",
            request=request,
            extra={
                "response_content_type": response.headers.get("content-type", "-"),
                "response_content_length": response.headers.get("content-length", "-"),
            },
        )
    return response


@app.get("/health", response_model=HealthResponse)
def health() -> HealthResponse:
    return HealthResponse(status="ok", service="api-gateway")


@app.get("/")
def login_ui() -> FileResponse:
    if not _LOGIN_UI_DIR.exists():
        raise HTTPException(status_code=404, detail="Login UI not found")
    return FileResponse(_LOGIN_UI_DIR / "index.html")


@app.get("/login")
def login_alias() -> FileResponse:
    return login_ui()


@app.get("/registry")
def registry() -> dict:
    return {"services": SERVICE_URLS}


@app.get("/operator/config")
def operator_ui_config() -> dict[str, Any]:
    return {
        "features": {
            "whatsapp": _env_flag("OPERATOR_WHATSAPP_ENABLED", default=False),
        }
    }


@app.get("/contracts")
def contracts() -> dict:
    return {
        "stage_1": ["/auth/*", "/users/*", "/health", "/audit/events"],
        "stage_2": [
            "/customers/*",
            "/interactions/*",
            "/queues/*",
            "/integrations/voice/events",
            "/integrations/telegram/webhook",
            "/integrations/whatsapp/webhook",
            "/integrations/webchat/messages",
            "/integrations/email/messages",
        ],
        "stage_3": [
            "/knowledge/*",
            "/reports/kpi",
            "/reports/export",
            "/supervisor/realtime",
        ],
        "stage_4": [
            "/recordings/*",
        ],
        "stage_5": [
            "/ivr/flows",
            "/ivr/sessions",
        ],
        "stage_6": [
            "/reports/agents/overview",
            "/reports/agents/timeseries",
            "/reports/coverage",
            "/reports/drilldown",
            "/reports/export",
            "/reports/facts/interactions",
            "/reports/kpi",
            "/reports/timeseries",
            "/reports/views",
            "/reports/views/*",
        ],
        "stage_8": [
            "/bus/outbox",
            "/bus/outbox/*",
        ],
        "stage_9": [
            "/asterisk/status",
            "/asterisk/events",
            "/asterisk/events/*",
        ],
        "stage_11": [
            "/asterisk/live-calls",
            "/asterisk/live-calls/*",
        ],
        "stage_12": [
            "/asterisk/recent-calls",
        ],
        "stage_14": [
            "/asterisk/browser-softphone/config",
        ],
        "stage_15": [
            "/integrations/telegram/bot/webhook",
            "/integrations/telegram/threads",
            "/integrations/telegram/threads/*",
            "/integrations/whatsapp/provider/webhook",
            "/integrations/whatsapp/threads",
            "/integrations/whatsapp/threads/*",
        ],
        "stage_16": [
            "/ai/telegram/threads/*",
            "/ai/whatsapp/threads/*",
            "/ai/analytics/overview",
            "/ai/analytics/timeseries",
            "/ai/analytics/voice-name-flow/overview",
            "/ai/analytics/voice-name-flow/timeseries",
            "/ai/analytics/drilldown",
            "/ai/analytics/sessions/*",
            "/ai/operator/config",
        ],
        "stage_17": [
            "/ai/voice/sessions/*",
            "/asterisk/live-calls/*/ai-summary",
        ],
        "stage_18": [
            "/api/v1/leads",
            "/api/v1/deals",
            "/api/v1/messages/inbound-webhook",
            "/api/v1/calls/inbound-webhook",
            "/api/v1/deals/*/workspace",
            "/api/v1/payments/webhook",
        ],
    }


@app.get("/operator")
def operator_ui() -> FileResponse:
    if not _OPERATOR_UI_DIR.exists():
        raise HTTPException(status_code=404, detail="Operator UI not found")
    return FileResponse(_OPERATOR_UI_DIR / "index.html")


@app.get("/dashboard")
def dashboard_ui() -> FileResponse:
    # Backward-compatible entrypoint used in production bookmarks.
    # Route dashboard directly to the sales cockpit so users can continue
    # their sales funnel workflow from legacy /dashboard links.
    return sales_ui()


@app.get("/supervisor")
def supervisor_ui() -> FileResponse:
    if not _SUPERVISOR_UI_DIR.exists():
        raise HTTPException(status_code=404, detail="Supervisor UI not found")
    return FileResponse(_SUPERVISOR_UI_DIR / "index.html")


@app.get("/analyst")
def analyst_ui() -> FileResponse:
    if not _ANALYST_UI_DIR.exists():
        raise HTTPException(status_code=404, detail="Analyst UI not found")
    return FileResponse(_ANALYST_UI_DIR / "index.html")


@app.get("/admin")
def admin_ui() -> FileResponse:
    if not _ADMIN_UI_DIR.exists():
        raise HTTPException(status_code=404, detail="Admin UI not found")
    return FileResponse(_ADMIN_UI_DIR / "index.html")


@app.get("/sales")
def sales_ui() -> FileResponse:
    if not _SALES_UI_DIR.exists():
        raise HTTPException(status_code=404, detail="Sales UI not found")
    return FileResponse(_SALES_UI_DIR / "index.html")


@app.get("/privacy")
def privacy_page() -> HTMLResponse:
    return HTMLResponse(_LEGAL_HTML["privacy"])


@app.get("/terms")
def terms_page() -> HTMLResponse:
    return HTMLResponse(_LEGAL_HTML["terms"])


@app.get("/data-deletion")
def data_deletion_page() -> HTMLResponse:
    return HTMLResponse(_LEGAL_HTML["data-deletion"])


async def _forward(method: str, service: str, path: str, request: Request) -> Response:
    base = SERVICE_URLS.get(service)
    if not base:
        raise HTTPException(status_code=404, detail="Unknown service")

    target_path = _resolve_service_path(service, path)
    url = f"{base.rstrip('/')}/{target_path}"
    headers = {
        "X-User": request.headers.get("X-User", ""),
        "X-Role": request.headers.get("X-Role", ""),
    }
    if request.headers.get("Authorization"):
        headers["Authorization"] = request.headers["Authorization"]
    if request.headers.get("X-Tenant-ID"):
        headers["X-Tenant-ID"] = request.headers["X-Tenant-ID"]
    if request.headers.get("X-Provider-Account-ID"):
        headers["X-Provider-Account-ID"] = request.headers["X-Provider-Account-ID"]
    if request.headers.get("X-Provider-Name"):
        headers["X-Provider-Name"] = request.headers["X-Provider-Name"]
    if request.headers.get("X-Telegram-Bot-Api-Secret-Token"):
        headers["X-Telegram-Bot-Api-Secret-Token"] = request.headers[
            "X-Telegram-Bot-Api-Secret-Token"
        ]
    if request.headers.get("X-WhatsApp-Webhook-Secret"):
        headers["X-WhatsApp-Webhook-Secret"] = request.headers["X-WhatsApp-Webhook-Secret"]
    if request.headers.get("X-Hub-Signature-256"):
        headers["X-Hub-Signature-256"] = request.headers["X-Hub-Signature-256"]
    if request.headers.get("Content-Type"):
        headers["Content-Type"] = request.headers["Content-Type"]

    payload: Any = None
    if method in {"POST", "PATCH", "PUT"}:
        payload = await request.body()
        if payload == b"":
            payload = None

    body_preview = "-"
    try:
        async with httpx.AsyncClient(timeout=20) as client:
            resp = await client.request(
                method,
                url,
                params=dict(request.query_params),
                content=payload,
                headers=headers,
            )
    except httpx.RequestError as exc:
        body_preview = _error_alert_body_preview(payload)
        _queue_telegram_error_alert(
            service=_error_alert_service_name(),
            method=method,
            path=f"/proxy/{service}/{path}",
            status=502,
            detail=f"{type(exc).__name__}: {exc}",
            request=request,
            extra={
                "upstream_service": service,
                "upstream_base_url": base,
                "upstream_target_path": target_path,
                "upstream_url": url,
                "request_body_preview": body_preview,
            },
        )
        logger.warning(
            "Upstream service unavailable: service=%s base_url=%s method=%s path=%s",
            service,
            base,
            method,
            path,
            exc_info=True,
        )
        return JSONResponse(
            status_code=502,
            content={
                "detail": "Upstream service unavailable",
                "service": service,
            },
        )

    if 300 <= resp.status_code < 400 and resp.headers.get("location"):
        return RedirectResponse(url=resp.headers["location"], status_code=resp.status_code)

    content_type = resp.headers.get("content-type", "")
    if "application/json" in content_type:
        return JSONResponse(status_code=resp.status_code, content=resp.json())
    if "text/html" in content_type:
        return HTMLResponse(status_code=resp.status_code, content=resp.text)
    if content_type.startswith("text/"):
        return Response(
            status_code=resp.status_code,
            content=resp.text,
            media_type=content_type.split(";", 1)[0],
        )
    if content_type.startswith("audio/") or "application/octet-stream" in content_type:
        passthrough_headers = {}
        if resp.headers.get("content-disposition"):
            passthrough_headers["Content-Disposition"] = resp.headers["content-disposition"]
        return Response(
            status_code=resp.status_code,
            content=resp.content,
            media_type=content_type.split(";", 1)[0],
            headers=passthrough_headers,
        )
    return JSONResponse(status_code=resp.status_code, content={"raw": resp.text})


@app.api_route("/proxy/{service}/{path:path}", methods=["GET", "POST", "PATCH", "PUT", "DELETE"])
async def proxy(service: str, path: str, request: Request) -> Response:
    return await _forward(request.method.upper(), service, path, request)
