"""FastAPI application entry point."""

from __future__ import annotations

import logging
import os as _os
import sys

# Force a known-safe value before Python subprocesses are spawned.
_os.environ["PYTHONHASHSEED"] = "random"

# Configure logging so INFO-level messages from all modules are visible.
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s")

# Configure structlog for JSON-formatted logging with run_id/node_id context.
try:
	import structlog
	structlog.configure(
		processors=[
			structlog.contextvars.merge_contextvars,
			structlog.processors.add_log_level,
			structlog.processors.TimeStamper(fmt="iso"),
			structlog.dev.ConsoleRenderer() if _os.environ.get("DSC_LOG_DEV") else structlog.processors.JSONRenderer(),
		],
		wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
		context_class=dict,
		logger_factory=structlog.PrintLoggerFactory(),
		cache_logger_on_first_use=True,
	)
except ImportError:
	pass  # structlog optional at import time

import uvicorn
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pathlib import Path

import httpx
from fastapi.responses import JSONResponse

from dsc_api.routers import agents, analytics, billing, codeintel, completions, compliance, connectors, cpg, credentials, creator, deploy, diagnostics, file, inline_edit, llm, marketplace, mcp, memory, news, noise_reports, notes, outreach, pentest, remote, research, rules, scaffold, scan, scan_file, scheduler, secrets, skills, social_auth, suppressions, tasks, templates, vcs, vuln, webhooks, widgets
from dsc_api.services import automation_scheduler, file_relay, global_memory, state_store
from dsc_api.services import llm_client
from dsc_api.services.auth_context import (
	ExecutionPrincipal,
	SERVICE_PRINCIPAL,
	set_current_principal,
)
from dsc_api.adapters import start_all as start_adapters, stop_all as stop_adapters
from dsc_api.websocket import remote_stream, scan_stream

logger = logging.getLogger(__name__)


def _run_optional_sync_step(phase: str, name: str, step) -> None:
	"""Run a non-scan lifecycle step without taking the scanner API down."""
	try:
		step()
	except Exception:
		logger.exception("Optional backend %s step failed: %s", phase, name)


async def _run_optional_async_step(phase: str, name: str, step) -> None:
	"""Run a non-critical shutdown step and log if it fails."""
	try:
		await step()
	except Exception:
		logger.exception("Optional backend %s step failed: %s", phase, name)


@asynccontextmanager
async def lifespan(app: FastAPI):
	"""Start the state store, rehydrate durable state, start scheduler and adapters on startup; shut down on exit."""
	await llm_client.startup()

	state_store.init()
	_run_optional_sync_step("startup", "initialize global memory", global_memory.init)

	# Rehydrate in-memory state from SQLite (Phase 2 §5.4: state durability)
	from dsc_api.services.task_orchestrator import rehydrate_tasks
	from dsc_api.services.agent_orchestrator import rehydrate_agents, rehydrate_swarms
	_run_optional_sync_step("startup", "rehydrate tasks", rehydrate_tasks)
	_run_optional_sync_step("startup", "rehydrate agents", rehydrate_agents)
	_run_optional_sync_step("startup", "rehydrate swarms", rehydrate_swarms)

	_run_optional_sync_step("startup", "start automation scheduler", automation_scheduler.start_scheduler)

	# Rehydrate scheduled tasks and re-register APScheduler jobs
	from dsc_api.routers.scheduler import ensure_table, rehydrate_scheduled_tasks
	_run_optional_sync_step("startup", "initialize scheduler tables", ensure_table)
	_run_optional_sync_step("startup", "rehydrate scheduled tasks", rehydrate_scheduled_tasks)

	# Start RSS auto-draft loop (polls enabled RSS sources on interval)
	from dsc_api.services.rss_autodraft import start_rss_autodraft
	_run_optional_sync_step("startup", "start RSS auto-draft", lambda: start_rss_autodraft(automation_scheduler.scheduler))

	# Rehydrate pentest sessions from disk
	from dsc_api.services.pentest_orchestrator import rehydrate_sessions
	_run_optional_sync_step("startup", "rehydrate pentest sessions", rehydrate_sessions)

	_run_optional_sync_step("startup", "start adapters", start_adapters)
	_run_optional_sync_step("startup", "start file relay", file_relay.start)
	yield
	_run_optional_sync_step("shutdown", "stop file relay", file_relay.stop)
	await _run_optional_async_step("shutdown", "stop adapters", stop_adapters)
	_run_optional_sync_step("shutdown", "stop automation scheduler", automation_scheduler.stop_scheduler)
	await llm_client.shutdown()


app = FastAPI(
    title="DSC Scanner API",
    description="Backend API for Deva Scanner desktop application",
    version="0.1.0",
    lifespan=lifespan,
)

# CORS - restrict to known local origins unless DSC_CORS_PERMISSIVE is set.
#
# Starlette's CORSMiddleware does literal string matches on allow_origins, so
# the previous entry "vscode-webview://*" never actually applied (the literal
# asterisk does not wildcard). The webview origin is therefore expressed via
# allow_origin_regex below. We also drop the bare "file://" origin which let
# any local file-system page issue cross-origin requests; legitimate first-
# party UIs come from the vscode-webview or vscode-file scheme instead.
_cors_origins = [
    "http://127.0.0.1:8000",
    "http://localhost:8000",
    "vscode-file://vscode-app",
]
if _os.environ.get("DSC_CORS_PERMISSIVE"):
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=False,
        allow_methods=["*"],
        allow_headers=["*"],
    )
else:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=_cors_origins,
        allow_origin_regex=r"^vscode-webview://.*$",
        allow_credentials=False,
        allow_methods=["*"],
        allow_headers=["*"],
    )


# ── Auth extraction middleware ─────────────────────────────────────────────
# Stores the raw Authorization header as request-scoped context so
# llm_client can forward it to the proxy.  Does NOT verify tokens —
# the proxy is the first trusted verifier.
#
# Phase 7: also extracts X-DSC-Request-Id, the client-generated idempotency
# key. The same id sent on a retry collapses to one Firestore usage_event
# row across the proxy.
@app.middleware("http")
async def auth_extraction_middleware(request: Request, call_next):
    from dsc_api.services.auth_context import (
        get_current_client_request_id,
        set_current_client_request_id,
    )

    auth_header = request.headers.get("authorization", "")
    if auth_header.lower().startswith("bearer "):
        token = auth_header[7:]
        principal = ExecutionPrincipal(kind="firebase", bearer_token=token)
    else:
        principal = SERVICE_PRINCIPAL
    set_current_principal(principal)

    client_request_id = (request.headers.get("x-dsc-request-id") or "").strip()
    set_current_client_request_id(client_request_id or None)

    try:
        response = await call_next(request)
    finally:
        set_current_principal(None)
        set_current_client_request_id(None)
    return response


# ── Forward upstream 429 responses ────────────────────────────────────────
# When the proxy returns 429, httpx.HTTPStatusError is raised by
# raise_for_status().  Forward the status code and Retry-After header
# so the frontend can display a meaningful message.
@app.exception_handler(httpx.HTTPStatusError)
async def _httpx_status_error_handler(request: Request, exc: httpx.HTTPStatusError):
    if exc.response.status_code == 429:
        headers: dict[str, str] = {}
        retry_after = exc.response.headers.get("retry-after")
        if retry_after:
            headers["Retry-After"] = retry_after
        return JSONResponse(
            status_code=429,
            content={"detail": "Rate limit exceeded. Please try again later."},
            headers=headers,
        )
    # For other upstream errors, return 502 with the upstream status
    return JSONResponse(
        status_code=502,
        content={"detail": f"Upstream error: {exc.response.status_code}"},
    )


# REST routers
app.include_router(scan_file.router, prefix="/api/scan/file", tags=["scan"])
app.include_router(scan.router, prefix="/api/scan", tags=["scan"])
app.include_router(vuln.router, prefix="/vuln", tags=["vuln"])
app.include_router(rules.router, prefix="/api/rules", tags=["rules"])
app.include_router(file.router, prefix="/api/file", tags=["file"])
app.include_router(vcs.router, prefix="/api/vcs", tags=["vcs"])
app.include_router(llm.router, prefix="/api/llm", tags=["llm", "security-llm"])
app.include_router(news.router, prefix="/api/news", tags=["news"])
app.include_router(notes.router, prefix="/api/notes", tags=["notes"])
app.include_router(remote.router, prefix="/api/remote", tags=["remote"])
app.include_router(tasks.router, prefix="/api/tasks", tags=["tasks"])
app.include_router(webhooks.router, prefix="/hooks", tags=["webhooks"])
app.include_router(connectors.router, prefix="/api/connectors", tags=["connectors"])
app.include_router(credentials.router, prefix="/api/credentials", tags=["credentials"])
app.include_router(agents.router, prefix="/api/agents", tags=["agents"])
app.include_router(diagnostics.router, prefix="/api/diagnostics", tags=["diagnostics"])
app.include_router(mcp.router, prefix="/api/mcp", tags=["mcp"])
app.include_router(skills.router, prefix="/api/skills", tags=["skills"])
app.include_router(research.router, prefix="/api/research", tags=["research"])
app.include_router(analytics.router, prefix="/api/analytics", tags=["analytics"])
app.include_router(outreach.router, prefix="/api/outreach", tags=["outreach"])
app.include_router(creator.router, prefix="/api/creator", tags=["creator"])
app.include_router(social_auth.router, prefix="/api/social", tags=["social-auth"])
app.include_router(billing.router, prefix="/api/billing", tags=["billing"])
app.include_router(completions.router, prefix="/api/llm/completions", tags=["completions"])
app.include_router(inline_edit.router, prefix="/api/llm/inline-edit", tags=["inline-edit"])
app.include_router(marketplace.router, prefix="/api/marketplace", tags=["marketplace"])
app.include_router(templates.router, prefix="/api/templates", tags=["templates"])
app.include_router(secrets.router, prefix="/api/secrets", tags=["secrets"])
app.include_router(deploy.router, prefix="/api/deploy", tags=["deploy"])
app.include_router(scaffold.router, prefix="/api/scaffold", tags=["scaffold"])
app.include_router(codeintel.router, prefix="/api/codeintel", tags=["codeintel"])
app.include_router(scheduler.router, prefix="/api/scheduler", tags=["scheduler"])
app.include_router(suppressions.router, prefix="/api/suppressions", tags=["suppressions"])
app.include_router(noise_reports.router, prefix="/api/noise-reports", tags=["noise-reports"])
app.include_router(compliance.router, prefix="/api/compliance", tags=["compliance"])
app.include_router(pentest.router, prefix="/api/pentest", tags=["pentest"])
app.include_router(cpg.router, tags=["cpg"])
app.include_router(memory.router, prefix="/api/memory", tags=["memory"])
app.include_router(widgets.router, prefix="/api/scanner", tags=["widgets"])
app.include_router(widgets.system_router, prefix="/api/system", tags=["widgets"])

# WebSocket endpoints
app.include_router(scan_stream.router, tags=["websocket"])
app.include_router(remote_stream.router, tags=["websocket"])

# Serve MediaPipe static assets for hand tracking
_mediapipe_dir = Path(__file__).resolve().parents[4] / "mediapipe"
if _mediapipe_dir.is_dir():
    app.mount("/mediapipe", StaticFiles(directory=str(_mediapipe_dir)), name="mediapipe")


def _is_frozen() -> bool:
    """Return True when running from a PyInstaller bundle."""
    return getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")


@app.get("/health")
async def health_check() -> dict[str, str]:
    """Health check endpoint."""
    scanner_root = ""
    try:
        import dsc

        scanner_root = str(Path(dsc.__file__).resolve().parent)
    except (ImportError, AttributeError):
        scanner_root = ""

    return {
        "status": "healthy",
        "service": "devseccode-scanner-backend",
        "source": "devseccode-ide-bundled",
        "runtime": "binary" if _is_frozen() else "python",
        "backend_root": str(Path(__file__).resolve().parents[3]),
        "scanner_root": scanner_root,
    }


def run() -> None:
    """Run the API server.

    Accepts ``--host`` and ``--port`` CLI arguments so the PyInstaller-bundled
    binary can be launched with ``dsc-backend --host 127.0.0.1 --port 8000``.
    """
    import argparse
    parser = argparse.ArgumentParser(description="DSC Scanner Backend")
    parser.add_argument("--host", default="127.0.0.1", help="Bind address")
    parser.add_argument("--port", type=int, default=8000, help="Bind port")
    args = parser.parse_args()

    uvicorn.run(
        "dsc_api.main:app",
        host=args.host,
        port=args.port,
        reload=False,
    )


if __name__ == "__main__":
    run()
