import logging
import re
import sys
from contextlib import asynccontextmanager
from importlib.metadata import PackageNotFoundError, version as package_version
from pathlib import Path
from threading import Lock
from typing import Final

if __package__ in {None, ""}:
    sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from fastapi import Request
from fastapi import FastAPI, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.responses import JSONResponse
import uvicorn

from app import models as _models  # noqa: F401 - ensure all SQLAlchemy models are registered
from app.api.access import router as access_router
from app.api.auth import router as auth_router
from app.api.client_access import router as client_access_router
from app.api.dashboard import router as dashboard_router
from app.api.editor import router as editor_router
from app.api.mix_calculator import router as mix_calculator_router
from app.api.mixes import router as mixes_router
from app.api.ordering import router as ordering_router
from app.api.ordering_admin import router as ordering_admin_router
from app.api.powerbi import router as powerbi_router
from app.api.product_costing import router as product_costing_router
from app.api.products import router as products_router
from app.api.public_v1 import router as public_v1_router
from app.api.raw_materials import router as raw_materials_router
from app.api.scenarios import router as scenarios_router
from app.api.throughput import router as throughput_router
from app.core.config import settings
from app.core.logging import (
    LoggingSettings,
    RequestTimer,
    configure_logging,
    debug,
    fatal,
    info,
    log_request,
    route_summary,
    section_heading,
    shutdown_summary,
    startup_banner,
    startup_status,
    success,
)
from app.db.session import Base, engine
from app.db.migrations import MigrationReport, bootstrap_schema, sync_product_visibility, sync_tenant_ids
from app.seed import seed_startup_basics


def _resolve_version() -> str:
    try:
        return package_version("data-entry-app-backend")
    except PackageNotFoundError:
        return "0.0.0"


APP_VERSION: Final[str] = _resolve_version()
_logging_settings = LoggingSettings(
    app_name=settings.app_name,
    app_env=settings.app_env,
    host=settings.host,
    port=settings.port,
    log_level=settings.log_level,
    log_verbose=settings.log_verbose,
    database_url=settings.database_url,
    version=APP_VERSION,
)

configure_logging(_logging_settings)

logger = logging.getLogger("data_entry_app.startup")
_database_ready = False
_database_ready_lock = Lock()
_requests_served = 0


def _origin_is_allowed(origin: str | None) -> bool:
    if not origin:
        return True

    if origin in settings.cors_allow_origins:
        return True

    if settings.cors_allow_origin_regex:
        return re.fullmatch(settings.cors_allow_origin_regex, origin) is not None

    return False


def ensure_database_ready() -> MigrationReport:
    global _database_ready

    if _database_ready:
        return MigrationReport()

    with _database_ready_lock:
        if _database_ready:
            return MigrationReport()

        schema_report = bootstrap_schema(engine, Base.metadata)
        seed_startup_basics()
        tenant_sync_report = sync_tenant_ids(engine)
        hidden_product_count = sync_product_visibility(engine)

        report = MigrationReport(
            created_tables=schema_report.created_tables,
            added_columns=schema_report.added_columns,
            synced_tenant_rows={
                **tenant_sync_report,
                **({"products_visibility": hidden_product_count} if hidden_product_count else {}),
            },
            resynced_sequences=schema_report.resynced_sequences,
        )
        logger.info("Database startup checks complete: %s", report.summary())
        _database_ready = True
        return report


@asynccontextmanager
async def lifespan(app: FastAPI):
    started = startup_status(_logging_settings)
    launch_time = RequestTimer()

    startup_banner(started)
    section_heading("Startup")
    info("Booting %s", settings.app_name, logger_name="data_entry_app.startup")

    section_heading("Configuration")
    success("Configuration loaded")
    info("CORS origins: %s", ", ".join(settings.cors_allow_origins), logger_name="data_entry_app.config")
    if settings.cors_allow_origin_regex:
        debug("CORS regex: %s", settings.cors_allow_origin_regex, logger_name="data_entry_app.config")

    section_heading("Database")
    try:
        report = ensure_database_ready()
    except Exception:
        fatal("Database startup failed", exc_info=True, logger_name="data_entry_app.database")
        raise
    success("Database connected")
    if report.has_changes():
        info(report.summary(), logger_name="data_entry_app.database")
    else:
        debug(report.summary(), logger_name="data_entry_app.database")

    section_heading("Routes")
    route_count, route_lines = route_summary(app.routes)
    success("Routes registered (%s endpoints)", route_count)
    if settings.log_verbose:
        for route_line in route_lines:
            debug(route_line, logger_name="data_entry_app.routes")

    section_heading("Services")
    success("HTTP API ready")
    if settings.docs_enabled:
        info("Docs available at /docs", logger_name="data_entry_app.services")
    else:
        info("Docs disabled in this environment", logger_name="data_entry_app.services")
    info("Health probe available at /health", logger_name="data_entry_app.services")

    yield

    shutdown_summary(
        uptime_seconds=launch_time.elapsed_ms / 1000,
        requests_served=_requests_served,
        host=settings.host,
        port=settings.port,
    )


app = FastAPI(
    title=settings.app_name,
    version=APP_VERSION,
    lifespan=lifespan,
    docs_url="/docs" if settings.docs_enabled else None,
    redoc_url=None,
    openapi_url="/openapi.json" if settings.docs_enabled else None,
)

app.add_middleware(TrustedHostMiddleware, allowed_hosts=list(settings.trusted_hosts) or ["*"])

app.add_middleware(
    CORSMiddleware,
    allow_origins=list(settings.cors_allow_origins),
    allow_origin_regex=settings.cors_allow_origin_regex or None,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type", "X-Requested-With"],
)

app.include_router(auth_router)
app.include_router(access_router)
app.include_router(client_access_router)
app.include_router(dashboard_router)
app.include_router(editor_router)
app.include_router(raw_materials_router)
app.include_router(mixes_router)
app.include_router(mix_calculator_router)
app.include_router(product_costing_router)
app.include_router(products_router)
app.include_router(scenarios_router)
app.include_router(throughput_router)
app.include_router(ordering_router)
app.include_router(ordering_admin_router)
app.include_router(powerbi_router)
app.include_router(public_v1_router)


@app.middleware("http")
async def log_http_requests(request: Request, call_next):
    global _requests_served

    timer = RequestTimer()
    try:
        response = await call_next(request)
    except Exception:
        log_request(
            method=request.method,
            path=request.url.path,
            status_code=500,
            duration_ms=timer.elapsed_ms,
            client=request.client.host if request.client else "-",
            content_length=None,
        )
        raise

    _requests_served += 1
    log_request(
        method=request.method,
        path=request.url.path,
        status_code=response.status_code,
        duration_ms=timer.elapsed_ms,
        client=request.client.host if request.client else "-",
        content_length=response.headers.get("content-length"),
    )
    return response


@app.middleware("http")
async def enforce_request_limits_and_csrf(request: Request, call_next):
    content_length = request.headers.get("content-length")
    if content_length:
        try:
            if int(content_length) > settings.request_body_max_bytes:
                return JSONResponse(
                    status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
                    content={"detail": "Request body is too large"},
                )
        except ValueError:
            pass

    if request.method in {"POST", "PUT", "PATCH", "DELETE"} and request.cookies:
        origin = request.headers.get("origin")
        if not _origin_is_allowed(origin):
            return JSONResponse(
                status_code=status.HTTP_403_FORBIDDEN,
                content={"detail": "Origin is not allowed"},
            )

    response = await call_next(request)
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        "img-src 'self' data:; "
        "style-src 'self' 'unsafe-inline'; "
        "script-src 'self'; "
        "font-src 'self' data:; "
        "connect-src 'self'; "
        # PDF previews/printing load a same-origin blob: URL into an iframe.
        # Without an explicit frame-src/child-src these fall back to default-src
        # ('self'), which blocks blob: and breaks the in-app print dialog.
        "frame-src 'self' blob:; "
        "child-src 'self' blob:; "
        "frame-ancestors 'self'; "
        "base-uri 'self'; "
        "form-action 'self'"
    )
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "SAMEORIGIN"
    response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
    if settings.app_env.lower() == "production":
        response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    return response


@app.exception_handler(HTTPException)
async def http_exception_handler(_: Request, exc: HTTPException):
    return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})


@app.exception_handler(Exception)
async def unhandled_exception_handler(_: Request, exc: Exception):
    fatal("Unhandled server error", exc_info=True, logger_name="data_entry_app.http")
    return JSONResponse(status_code=500, content={"detail": "Internal server error"})


@app.get("/")
def root():
    return {
        "app": settings.app_name,
        "message": "Use the operator frontend to sign in, manage raw materials, and review downstream mix and product costs.",
        "workflow": [
            "Sign in as an operator",
            "Update raw material prices or add new materials",
            "Review mix master recalculations",
            "Confirm finished product pricing outputs",
        ],
        "endpoints": {
            "client_login": "/api/auth/client/login",
            "admin_login": "/api/auth/admin/login",
            "raw_materials": "/api/raw-materials",
            "mixes": "/api/mixes",
            "mix_calculator": "/api/mix-calculator",
            "products": "/api/products",
            "scenarios": "/api/scenarios",
            "operations_throughput": "/api/throughput",
            "client_access": "/api/client-access",
            "powerbi_throughput": "/api/v1/throughput",
            "docs": "/docs",
        },
    }


@app.get("/health")
def healthcheck():
    return {"status": "ok"}


if __name__ == "__main__":
    report = ensure_database_ready()
    success("Database startup checks complete: %s", report.summary(), logger_name="data_entry_app.startup")
    uvicorn.run(
        app,
        host=settings.host,
        port=settings.port,
        access_log=False,
    )
