import logging
import time
import traceback as tb_module
from pathlib import Path

from quart import Quart, g, jsonify, make_response, request, send_from_directory

from scribe.config import Config
from scribe.routes.admin import admin_bp
from scribe.routes.api import api
from scribe.routes.auth import auth_bp
from scribe.routes.export import export_bp
from scribe.routes.notes import notes_bp
from scribe.routes.milestones import milestones_bp
from scribe.routes.task_logs import task_logs_bp
from scribe.routes.projects import projects_bp
from scribe.routes.settings import settings_bp
from scribe.routes.tasks import tasks_bp
from scribe.routes.groups import groups_bp
from scribe.routes.shares import shares_bp
from scribe.routes.in_app_notifications import notifications_bp
from scribe.routes.users import users_bp
from scribe.routes.api_keys import api_keys_bp
from scribe.routes.search import search_bp
from scribe.routes.profile import profile_bp
from scribe.routes.knowledge import knowledge_bp
from scribe.routes.rulebooks import rulebooks_bp
from scribe.routes.plugin import plugin_bp
from scribe.routes.design_systems import design_systems_bp
from scribe.routes.trash import trash_bp
from scribe.routes.dashboard import dashboard_bp
from scribe.routes.systems import systems_bp
from scribe.routes.snippets import snippets_bp
from scribe.routes.webhooks import webhooks_bp
from scribe.mcp import mount_mcp

STATIC_DIR = Path(__file__).parent / "static"
logger = logging.getLogger(__name__)


def create_app() -> Quart:
    # Configure logging
    logging.basicConfig(
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        level=getattr(logging, Config.LOG_LEVEL.upper(), logging.INFO),
    )

    # Validate config early so misconfigurations surface immediately with clear messages
    try:
        Config.validate()
    except ValueError as exc:
        logging.getLogger(__name__).error("Invalid configuration:\n%s", exc)
        raise

    app = Quart(__name__, static_folder=None)
    app.secret_key = Config.SECRET_KEY
    app.config["SESSION_COOKIE_HTTPONLY"] = True
    app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
    app.config["SESSION_COOKIE_SECURE"] = Config.SECURE_COOKIES

    if Config.SECRET_KEY == "dev-secret-change-me":
        logger.warning(
            "SECRET_KEY is set to the default value — session cookies are insecure. "
            "Set SECRET_KEY or SECRET_KEY_FILE for production use."
        )

    if not Config.TRUST_PROXY_HEADERS:
        logger.warning(
            "TRUST_PROXY_HEADERS is not set. If this instance is behind a reverse proxy "
            "(nginx, Caddy, Traefik) set TRUST_PROXY_HEADERS=true so rate limiting uses "
            "real client IPs rather than the proxy IP."
        )

    app.register_blueprint(admin_bp)
    app.register_blueprint(api)
    app.register_blueprint(auth_bp)
    app.register_blueprint(export_bp)
    app.register_blueprint(milestones_bp)
    app.register_blueprint(notes_bp)
    app.register_blueprint(projects_bp)
    app.register_blueprint(settings_bp)
    app.register_blueprint(task_logs_bp)
    app.register_blueprint(tasks_bp)
    app.register_blueprint(groups_bp)
    app.register_blueprint(shares_bp)
    app.register_blueprint(notifications_bp)
    app.register_blueprint(users_bp)
    app.register_blueprint(api_keys_bp)
    app.register_blueprint(search_bp)
    app.register_blueprint(profile_bp)
    app.register_blueprint(knowledge_bp)
    app.register_blueprint(rulebooks_bp)
    app.register_blueprint(plugin_bp)
    app.register_blueprint(design_systems_bp)
    app.register_blueprint(trash_bp)
    app.register_blueprint(dashboard_bp)
    app.register_blueprint(systems_bp)
    app.register_blueprint(snippets_bp)
    app.register_blueprint(webhooks_bp)

    @app.before_request
    async def before_request():
        g.request_start = time.monotonic()

    @app.after_request
    async def after_request(response):
        duration = time.monotonic() - getattr(g, "request_start", time.monotonic())
        duration_ms = round(duration * 1000, 1)
        # Downgrade noisy high-frequency / static paths to DEBUG
        _quiet = (
            request.path in {"/api/health", "/api/chat/status"}
            or request.path.startswith(("/static/", "/assets/", "/sw.js", "/manifest.json"))
        )
        log_fn = logger.debug if _quiet else logger.info
        log_fn(
            "%s %s %s %.1fms",
            request.method,
            request.path,
            response.status_code,
            duration_ms,
        )

        # Log usage for API requests (skip logs endpoint to avoid recursion)
        if request.path.startswith("/api/") and not request.path.startswith("/api/admin/logs"):
            try:
                from scribe.services.logging import log_usage

                user = getattr(g, "user", None)
                await log_usage(
                    user_id=user.id if user else None,
                    username=user.username if user else None,
                    endpoint=request.path,
                    method=request.method,
                    status_code=response.status_code,
                    duration_ms=duration_ms,
                )
            except Exception:
                logger.debug("Failed to log usage", exc_info=True)

        response.headers.setdefault("X-Content-Type-Options", "nosniff")
        response.headers.setdefault("X-Frame-Options", "DENY")
        response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
        response.headers.setdefault(
            "Content-Security-Policy",
            "default-src 'self'; "
            "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; "
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
            "img-src 'self' data: blob:; "
            "connect-src 'self'; "
            "font-src 'self' data: https://fonts.gstatic.com; "
            "object-src 'none'; "
            "base-uri 'self'; worker-src 'self' blob:;"
        )

        return response

    @app.before_serving
    async def startup():
        import asyncio

        from scribe.services.auth import start_auth_token_retention_loop
        from scribe.services.embeddings import backfill_note_embeddings
        from scribe.services.logging import start_log_retention_loop
        from scribe.services.notifications import start_notification_loop

        start_log_retention_loop()
        start_notification_loop()
        start_auth_token_retention_loop()

        # Backfill embeddings for any notes that don't have one. Runs in the
        # background so it never blocks the server from accepting requests.
        async def _delayed_backfill() -> None:
            try:
                await backfill_note_embeddings()
            except Exception:
                logger.warning("Embedding backfill failed", exc_info=True)
            # Snippets written before migration 0070 have no `notes.data` mirror,
            # and the location reverse lookup queries that column — an unfilled
            # row would read as "no snippet here" rather than as a gap. Separate
            # try block so neither backfill can skip the other.
            try:
                from scribe.services.snippets import backfill_snippet_data
                await backfill_snippet_data()
            except Exception:
                logger.warning("Snippet data backfill failed", exc_info=True)

        asyncio.create_task(_delayed_backfill())

        # Recurrence scheduler (recurring-task spawn every 15m)
        from scribe.services.recurrence_scheduler import start_recurrence_scheduler
        start_recurrence_scheduler(asyncio.get_running_loop())

        # Version-pinning scheduler (daily auto-pin scan at 03:00 UTC)
        from scribe.services.version_pinning_scheduler import (
            start_version_pinning_scheduler,
        )
        start_version_pinning_scheduler(asyncio.get_running_loop())

        # Trash retention scheduler (daily expired-trash purge at 03:30 UTC)
        from scribe.services.trash_scheduler import start_trash_scheduler
        start_trash_scheduler(asyncio.get_running_loop())

        # DB maintenance scheduler (daily targeted VACUUM ANALYZE, default 04:00 UTC)
        from scribe.services.db_maintenance_scheduler import (
            get_maintenance_hour,
            start_db_maintenance_scheduler,
        )
        start_db_maintenance_scheduler(
            asyncio.get_running_loop(), await get_maintenance_hour()
        )

        # Diagnostic instrumentation — heartbeat, signal handlers, asyncio
        # exception hook. Cheap (~1 log line/min), high diagnostic value when
        # the app crashes mysteriously. See services/diagnostics.py.
        from scribe.services.diagnostics import start_diagnostics
        start_diagnostics(asyncio.get_running_loop())

    @app.after_serving
    async def shutdown():
        from scribe.services.recurrence_scheduler import stop_recurrence_scheduler
        stop_recurrence_scheduler()
        from scribe.services.version_pinning_scheduler import (
            stop_version_pinning_scheduler,
        )
        stop_version_pinning_scheduler()
        from scribe.services.trash_scheduler import stop_trash_scheduler
        stop_trash_scheduler()
        from scribe.services.db_maintenance_scheduler import (
            stop_db_maintenance_scheduler,
        )
        stop_db_maintenance_scheduler()
        from scribe.services.diagnostics import stop_diagnostics
        stop_diagnostics()

    @app.route("/")
    async def serve_index():
        resp = await make_response(
            await send_from_directory(STATIC_DIR, "index.html")
        )
        resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
        return resp

    # File extensions that belong to the SPA or its assets.
    # Anything else with an extension is not a valid app path and gets a hard 404.
    _SPA_EXTENSIONS = {
        "", ".html", ".js", ".css", ".ico", ".png", ".jpg", ".jpeg",
        ".svg", ".webp", ".woff", ".woff2", ".ttf", ".otf", ".map", ".json",
    }

    @app.errorhandler(404)
    async def handle_404(error):
        # Return JSON 404 for API routes
        if request.path.startswith("/api/"):
            return jsonify({"error": "Not found"}), 404

        # Try to serve a real static file first
        path = request.path.lstrip("/")
        file_path = STATIC_DIR / path
        if path and file_path.is_file():
            return await send_from_directory(STATIC_DIR, path)

        # Reject paths with file extensions the app doesn't serve.
        # This turns scanner probes (.php, .asp, .cgi, etc.) into honest 404s
        # instead of serving them the Vue SPA with a misleading 200.
        from pathlib import PurePosixPath
        suffix = PurePosixPath(request.path).suffix.lower()
        if suffix not in _SPA_EXTENSIONS:
            return "", 404

        # SPA fallback for clean client-side routes (/notes/123, /chat, etc.)
        resp = await make_response(
            await send_from_directory(STATIC_DIR, "index.html")
        )
        resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
        return resp

    @app.errorhandler(500)
    async def handle_500(error):
        logger.exception("Internal server error on %s %s", request.method, request.path)

        try:
            from scribe.services.logging import log_error

            user = getattr(g, "user", None)
            await log_error(
                user_id=user.id if user else None,
                username=user.username if user else None,
                endpoint=request.path,
                method=request.method,
                error_type=type(error).__name__,
                error_message=str(error),
                traceback=tb_module.format_exc(),
            )
        except Exception:
            logger.debug("Failed to log error", exc_info=True)

        if request.path.startswith("/api/"):
            return jsonify({"error": "Internal server error"}), 500
        return "Internal Server Error", 500

    mount_mcp(app)
    return app
