# import logging - replaced with loguru
import os
from pathlib import Path
from importlib import resources as importlib_resources

from flask import (
    Flask,
    Request,
    abort,
    jsonify,
    make_response,
    request,
    send_from_directory,
)
from flask_wtf.csrf import CSRFProtect
from werkzeug.middleware.proxy_fix import ProxyFix
from loguru import logger
from local_deep_research.settings.logger import log_settings

from ..utilities.log_utils import InterceptHandler
from ..security import SecurityHeaders, get_security_default
from ..security.egress.policy import DEFAULT_EGRESS_SCOPE
from ..security.rate_limiter import limiter
from ..security.file_upload_validator import FileUploadValidator
from ..security.web_middleware import (
    SecureCookieMiddleware,
    ServerHeaderMiddleware,
)

# Removed DB_PATH import - using per-user databases now
from .services.socket_service import SocketIOService


class DiskSpoolingRequest(Request):
    """Custom Request class that spools large file uploads to disk.

    This prevents memory exhaustion from large multipart uploads by writing
    files larger than max_form_memory_size to temporary files on disk instead
    of keeping them in memory.

    Security fix for issue #1176: a request right up against the per-file ×
    per-request limits could otherwise consume tens of GB of memory in
    one go.  The spool-to-disk threshold keeps memory bounded regardless
    of how high the per-file cap (FileUploadValidator.MAX_FILE_SIZE) is set.
    """

    # Files larger than 5MB are spooled to disk instead of memory
    max_form_memory_size = 5 * 1024 * 1024  # 5MB threshold


def create_app():
    """
    Create and configure the Flask application.

    Returns:
        tuple: (app, socketio) - The configured Flask app and SocketIO instance
    """
    # Route stdlib loggers through loguru via InterceptHandler.
    # Guard against handler duplication when create_app() is called multiple
    # times (e.g. in tests).
    import logging

    werkzeug_logger = logging.getLogger("werkzeug")
    werkzeug_logger.setLevel(
        logging.WARNING
    )  # Suppress verbose per-request logs
    if not any(
        isinstance(h, InterceptHandler) for h in werkzeug_logger.handlers
    ):
        werkzeug_logger.addHandler(InterceptHandler())

    # APScheduler logs job execution results (success/failure) to its own
    # logger hierarchy.  Without an InterceptHandler the WARNING+ messages
    # only reach Python's lastResort handler as unformatted stderr.
    # Level is WARNING (not INFO) because job functions already log their
    # own progress via loguru — APScheduler's INFO messages would be redundant.
    apscheduler_logger = logging.getLogger("apscheduler")
    apscheduler_logger.setLevel(logging.WARNING)
    if not any(
        isinstance(h, InterceptHandler) for h in apscheduler_logger.handlers
    ):
        apscheduler_logger.addHandler(InterceptHandler())

    logger.info("Initializing Local Deep Research application...")

    try:
        # Get directories based on package installation
        PACKAGE_DIR = importlib_resources.files("local_deep_research") / "web"
        with importlib_resources.as_file(PACKAGE_DIR) as package_dir:
            STATIC_DIR = (package_dir / "static").as_posix()
            TEMPLATE_DIR = (package_dir / "templates").as_posix()

        # Initialize Flask app with package directories
        # Set static_folder to None to disable Flask's built-in static handling
        # We'll use our custom static route instead to handle dist folder
        app = Flask(__name__, static_folder=None, template_folder=TEMPLATE_DIR)
        # Store static dir for custom handling
        app.config["STATIC_DIR"] = STATIC_DIR
        logger.debug(f"Using package static path: {STATIC_DIR}")
        logger.debug(f"Using package template path: {TEMPLATE_DIR}")
    except Exception:
        # Fallback for development
        logger.exception("Package directories not found, using fallback paths")
        # Set static_folder to None to disable Flask's built-in static handling
        app = Flask(
            __name__,
            static_folder=None,
            template_folder=str(Path("templates").resolve()),
        )
        # Store static dir for custom handling
        app.config["STATIC_DIR"] = str(Path("static").resolve())

    # Use custom Request class that spools large uploads to disk
    # This prevents memory exhaustion from large file uploads (issue #1176)
    app.request_class = DiskSpoolingRequest

    # Middleware stack (wrapped innermost -> outermost; runs in reverse at
    # request time):
    #   1. SecureCookieMiddleware: adds Secure flag iff wsgi.url_scheme=https.
    #      Wrapped INSIDE ProxyFix so it reads the post-rewrite scheme.
    #   2. ProxyFix: translates X-Forwarded-* into REMOTE_ADDR / wsgi.url_scheme.
    #   3. ServerHeaderMiddleware: strips Server header (outermost).
    app.wsgi_app = SecureCookieMiddleware(app.wsgi_app, app)  # type: ignore[method-assign]
    app.wsgi_app = ProxyFix(  # type: ignore[method-assign]
        app.wsgi_app,
        x_for=1,  # Trust 1 proxy for X-Forwarded-For
        x_proto=1,  # Trust 1 proxy for X-Forwarded-Proto (http/https)
        x_host=0,  # Don't trust X-Forwarded-Host (security)
        x_port=0,  # Don't trust X-Forwarded-Port (security)
        x_prefix=0,  # Don't trust X-Forwarded-Prefix (security)
    )
    app.wsgi_app = ServerHeaderMiddleware(app.wsgi_app)  # type: ignore[method-assign]

    # CI/test-only request forensics for the #4431 navigation-stall hunts:
    # logs every request's arrival + duration so a silent log window can
    # be attributed to "request never arrived" vs "request hung in app".
    if os.environ.get("CI") or os.environ.get("TESTING"):
        from .utils.request_timing import RequestTimingMiddleware

        app.wsgi_app = RequestTimingMiddleware(app.wsgi_app)  # type: ignore[method-assign]
        logger.info("Request-timing forensics middleware enabled (CI/TESTING)")

    # App configuration
    # Generate or load a unique SECRET_KEY per installation
    import secrets
    from ..config.paths import get_data_directory

    secret_key_file = Path(get_data_directory()) / ".secret_key"
    secret_key_file.parent.mkdir(parents=True, exist_ok=True)
    new_key = secrets.token_hex(32)
    try:
        fd = os.open(
            str(secret_key_file), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600
        )
        try:
            os.write(fd, new_key.encode())
        finally:
            os.close(fd)
        app.config["SECRET_KEY"] = new_key
        logger.info("Generated new SECRET_KEY for this installation")
    except FileExistsError:
        try:
            with open(secret_key_file, "r", encoding="utf-8") as f:
                app.config["SECRET_KEY"] = f.read().strip()
        except Exception:
            logger.warning("Could not read secret key file")
            app.config["SECRET_KEY"] = new_key
    except OSError:
        logger.warning("Could not save secret key file")
        app.config["SECRET_KEY"] = new_key
    # Session cookie security settings
    # SECURE flag is added dynamically based on request context (see after_request below)
    # This allows localhost HTTP to work for development while keeping production secure
    #
    # Check if explicitly in testing mode (for backwards compatibility)
    is_testing = (
        os.getenv("CI")
        or os.getenv("TESTING")
        or os.getenv("PYTEST_CURRENT_TEST")
        or app.debug
    )
    # Set to False - we add Secure flag dynamically in after_request handler
    # Exception: if TESTING mode is active, we never add Secure flag
    app.config["SESSION_COOKIE_SECURE"] = False
    app.config["LDR_TESTING_MODE"] = bool(is_testing)  # Store for after_request
    app.config["SESSION_COOKIE_HTTPONLY"] = (
        True  # Prevent JavaScript access (XSS mitigation)
    )
    app.config["SESSION_COOKIE_SAMESITE"] = "Lax"  # CSRF protection
    # Set max cookie lifetime for permanent sessions (when session.permanent=True).
    # This applies to "remember me" sessions; non-permanent sessions expire on browser close.
    remember_me_days = get_security_default(
        "security.session_remember_me_days", 30
    )
    app.config["PERMANENT_SESSION_LIFETIME"] = remember_me_days * 24 * 3600
    # PREFERRED_URL_SCHEME affects URL generation (url_for), not request.is_secure
    app.config["PREFERRED_URL_SCHEME"] = "https"

    # File upload security limits - calculated from FileUploadValidator constants
    app.config["MAX_CONTENT_LENGTH"] = (
        FileUploadValidator.MAX_FILES_PER_REQUEST
        * FileUploadValidator.MAX_FILE_SIZE
    )

    # Initialize CSRF protection
    # Explicitly enable CSRF protection (don't rely on implicit Flask-WTF behavior)
    app.config["WTF_CSRF_ENABLED"] = True
    CSRFProtect(app)
    # Exempt Socket.IO from CSRF protection
    # Note: Flask-SocketIO handles CSRF internally, so we don't need to exempt specific views

    # Initialize security headers middleware
    SecurityHeaders(app)

    # Initialize rate limiting for security (brute force protection)
    # Uses imported limiter from security.rate_limiter module
    # Rate limiting is disabled in CI via enabled callable in rate_limiter.py
    # Also set app config to ensure Flask-Limiter respects our settings
    from ..settings.env_registry import is_rate_limiting_enabled

    app.config["RATELIMIT_ENABLED"] = is_rate_limiting_enabled()
    app.config["RATELIMIT_STRATEGY"] = "moving-window"
    limiter.init_app(app)

    # Custom error handler for rate limit exceeded (429)
    @app.errorhandler(429)
    def ratelimit_handler(e):
        # Import here to avoid circular imports
        from ..security.rate_limiter import get_client_ip

        # Audit logging for security monitoring
        # Use get_client_ip() to get the real IP behind proxies
        logger.warning(
            f"Rate limit exceeded: endpoint={request.endpoint} "
            f"ip={get_client_ip()} "
            f"user_agent={request.headers.get('User-Agent', 'unknown')}"
        )
        return jsonify(
            error="Too many requests",
            message="Too many attempts. Please try again later.",
        ), 429

    # Note: Dynamic cookie security is handled by SecureCookieMiddleware (WSGI level)
    # This is necessary because Flask's session cookies are set AFTER after_request handlers
    # The middleware wrapping happens below near ProxyFix

    # Note: CSRF exemptions for API blueprints are applied after blueprint
    # registration below (search for "CSRF exemptions" in this file).

    # Database configuration - Using per-user databases now
    # No shared database configuration needed
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
    app.config["SQLALCHEMY_ECHO"] = False

    # Per-user databases are created automatically via encrypted_db.py

    # Log data location and security information
    from ..config.paths import get_data_directory
    from ..database.encrypted_db import db_manager

    data_dir = get_data_directory()
    logger.info("=" * 60)
    logger.info("DATA STORAGE INFORMATION")
    logger.info("=" * 60)
    logger.info(f"Data directory: {data_dir}")
    logger.info(
        "Databases: Per-user encrypted databases in encrypted_databases/"
    )

    # Check if using custom location
    from local_deep_research.settings.manager import SettingsManager

    settings_manager = SettingsManager()
    custom_data_dir = settings_manager.get_setting("bootstrap.data_dir")
    if custom_data_dir:
        logger.info(
            f"Using custom data location via LDR_DATA_DIR: {custom_data_dir}"
        )
    else:
        logger.info("Using default platform-specific data location")

    # Display security status based on actual SQLCipher availability
    if db_manager.has_encryption:
        logger.info(
            "SECURITY: Databases are encrypted with SQLCipher. Ensure appropriate file system permissions are set on the data directory."
        )
    else:
        logger.warning(
            "SECURITY NOTICE: SQLCipher is not available - databases are NOT encrypted. "
            "Install SQLCipher for database encryption. Ensure appropriate file system permissions are set on the data directory."
        )

    logger.info(
        "TIP: You can change the data location by setting the LDR_DATA_DIR environment variable."
    )
    logger.info("=" * 60)

    # Initialize Vite helper for asset management
    from .utils.vite_helper import vite

    vite.init_app(app)

    # Initialize Theme helper for auto-detecting themes from CSS
    from .utils.theme_helper import theme_helper

    theme_helper.init_app(app)

    # Generate combined themes.css from individual theme files
    from .themes import theme_registry

    try:
        static_dir = Path(app.config.get("STATIC_DIR", "static"))
        themes_css_path = static_dir / "css" / "themes.css"
        combined_css = theme_registry.get_combined_css()
        themes_css_path.write_text(combined_css, encoding="utf-8")
        logger.debug(
            f"Generated themes.css with {len(theme_registry.themes)} themes"
        )
    except PermissionError:
        logger.warning(
            f"Cannot write themes.css to {themes_css_path}. "
            "Theme CSS will need to be pre-generated."
        )
    except Exception:
        logger.exception("Error generating combined themes.css")

    # Register socket service
    socket_service = SocketIOService(app=app)

    # Initialize news subscription scheduler
    try:
        # News tables are now created per-user in their encrypted databases
        logger.info(
            "News tables will be created in per-user encrypted databases"
        )

        # Check if scheduler is enabled BEFORE importing/initializing
        # Use env registry which handles both env vars and settings
        from ..settings.env_registry import get_env_setting

        scheduler_enabled = get_env_setting("news.scheduler.enabled", True)
        logger.info(f"News scheduler enabled: {scheduler_enabled}")

        if scheduler_enabled:
            # Only import and initialize if enabled
            from ..scheduler.background import (
                get_background_job_scheduler,
            )
            from ..settings.manager import SettingsManager

            # Get system settings for scheduler configuration (if not already loaded)
            if "settings_manager" not in locals():
                settings_manager = SettingsManager()

            # Get scheduler instance and initialize with settings
            scheduler = get_background_job_scheduler()
            scheduler.initialize_with_settings(settings_manager)
            scheduler.set_app(app)
            scheduler.start()
            app.background_job_scheduler = scheduler  # type: ignore[attr-defined]
            logger.info("News scheduler started with activity-based tracking")
        else:
            # Don't initialize scheduler if disabled
            app.background_job_scheduler = None  # type: ignore[attr-defined]
            logger.info("News scheduler disabled - not initializing")
    except Exception:
        logger.exception("Failed to initialize news scheduler")
        app.background_job_scheduler = None  # type: ignore[attr-defined]

    # Apply middleware
    logger.info("Applying middleware...")
    apply_middleware(app)
    logger.info("Middleware applied successfully")

    # Register blueprints
    logger.info("Registering blueprints...")
    register_blueprints(app)
    logger.info("Blueprints registered successfully")

    # Register error handlers
    logger.info("Registering error handlers...")
    register_error_handlers(app)
    logger.info("Error handlers registered successfully")

    # Start the queue processor v2 (uses encrypted databases)
    # Always start the processor - it will handle per-user queue modes
    logger.info("Starting queue processor v2...")
    from .queue.processor_v2 import queue_processor

    queue_processor.start()
    logger.info("Started research queue processor v2")

    logger.info("App factory completed successfully")

    return app, socket_service


def apply_middleware(app):
    """Apply middleware to the Flask app."""

    # Import auth decorators and middleware
    logger.info("Importing cleanup_middleware...")
    from .auth.cleanup_middleware import cleanup_completed_research

    logger.info("Importing database_middleware...")
    from .auth.database_middleware import ensure_user_database

    logger.info("Importing decorators...")
    from .auth.decorators import inject_current_user

    logger.info("Importing queue_middleware...")
    from .auth.queue_middleware import process_pending_queue_operations

    logger.info("Importing queue_middleware_v2...")
    from .auth.queue_middleware_v2 import notify_queue_processor

    logger.info("Importing session_cleanup...")
    from .auth.session_cleanup import cleanup_stale_sessions

    logger.info("All middleware imports completed")

    # Register authentication middleware
    # First clean up stale sessions
    app.before_request(cleanup_stale_sessions)
    # Then ensure database is open for authenticated users
    app.before_request(ensure_user_database)
    # Then inject current user into g
    app.before_request(inject_current_user)
    # Clean up completed research records
    app.before_request(cleanup_completed_research)
    # Process any pending queue operations for this user (direct mode)
    app.before_request(process_pending_queue_operations)
    # Notify queue processor of user activity (queue mode)
    app.before_request(notify_queue_processor)

    logger.info("All middleware registered")

    # Note: log-queue flushing is handled asynchronously by the
    # background daemon started in web/app.py::main() (see
    # start_log_queue_processor). We deliberately do NOT register
    # flush_log_queue as a before_request handler: doing so makes
    # every request (including GET /auth/login) synchronously wait
    # on _write_log_to_database, which opens a user DB session and
    # blocks if the shared connection pool is wedged. At-exit
    # draining is still handled via atexit in web/app.py.

    # Inject backend constants into Jinja2 templates for frontend JS.
    # This is the Flask-documented pattern for sharing Python enums with JavaScript.
    # Source of truth: src/local_deep_research/constants.py::ResearchStatus
    # Frontend helpers: src/local_deep_research/web/static/js/config/constants.js
    # Template injection: src/local_deep_research/web/templates/base.html
    from ..constants import (
        HISTORY_LOGS_DEFAULT_LIMIT,
        HISTORY_LOGS_HARD_CAP,
        ResearchStatus,
    )

    @app.context_processor
    def inject_frontend_constants():
        terminal = [
            ResearchStatus.COMPLETED,
            ResearchStatus.SUSPENDED,
            ResearchStatus.FAILED,
            ResearchStatus.ERROR,
            ResearchStatus.CANCELLED,
        ]
        # Make the active egress scope available to every template so base.html
        # can render it onto <body data-scope=…>. Scope-aware CSS in styles.css
        # picks it up to color the research card, chat input, etc. Falls back
        # to the registered default scope if anything goes wrong — fail open
        # visually, never crash a page render over a styling cue.
        scope = DEFAULT_EGRESS_SCOPE
        try:
            from flask import session as flask_session
            from ..database.session_context import get_user_db_session
            from ..utilities.db_utils import get_settings_manager

            username = flask_session.get("username") if flask_session else None
            if username:
                with get_user_db_session(username) as db_session:
                    if db_session:
                        sm = get_settings_manager(db_session, username)
                        scope = (
                            sm.get_setting(
                                "policy.egress_scope", DEFAULT_EGRESS_SCOPE
                            )
                            or DEFAULT_EGRESS_SCOPE
                        )
        except Exception:
            logger.debug(
                "Failed to read egress scope for template context",
                exc_info=True,
            )

        return {
            "research_status_enum": {m.name: m.value for m in ResearchStatus},
            "research_terminal_states": [str(s) for s in terminal],
            "log_limits": {
                "default": HISTORY_LOGS_DEFAULT_LIMIT,
                "hard_cap": HISTORY_LOGS_HARD_CAP,
            },
            "egress_scope": scope,
        }

    # Clean up database sessions after each request
    @app.teardown_appcontext
    def cleanup_db_session(exception=None):
        """Clean up database session after each request to avoid cross-thread issues."""
        from flask import g

        session = g.pop("db_session", None)
        if session is not None:
            try:
                session.rollback()
            except Exception:
                logger.warning(
                    "Error rolling back request session during cleanup"
                )
            try:
                session.close()
            except Exception:
                logger.warning("Error closing request session during cleanup")

        # Sweep credential entries for dead threads. Multiple trigger
        # points (here, processor_v2, and connection_cleanup scheduler)
        # ensure sweeps happen regardless of traffic patterns.
        try:
            from ..database.thread_local_session import cleanup_dead_threads

            cleanup_dead_threads()
        except Exception:
            logger.debug("Error during dead thread sweep", exc_info=True)

        # Clean up any thread-local database session that may have been created
        # via get_metrics_session() fallback in session_context.py (e.g. background
        # threads or error paths where g.db_session was unavailable).
        try:
            from ..database.thread_local_session import cleanup_current_thread

            cleanup_current_thread()
        except Exception:
            logger.debug(
                "Error during thread-local session cleanup", exc_info=True
            )

    # Add a middleware layer to handle abrupt disconnections
    @app.before_request
    def handle_websocket_requests():
        if request.path.startswith("/socket.io"):
            try:
                if not request.environ.get("werkzeug.socket"):
                    return None
            except Exception:
                logger.exception("WebSocket preprocessing error")
                # Return empty response to prevent further processing
                return "", 200
        return None

    # Note: CORS headers for API routes are now handled by SecurityHeaders middleware
    # (see src/local_deep_research/security/security_headers.py)


def register_blueprints(app):
    """Register blueprints with the Flask app."""

    # Import blueprints
    logger.info("Importing blueprints...")

    # Import benchmark blueprint
    from ..benchmarks.web_api.benchmark_routes import benchmark_bp

    logger.info("Importing API blueprint...")
    from .api import api_blueprint  # Import the API blueprint

    logger.info("Importing auth blueprint...")
    from .auth import auth_bp  # Import the auth blueprint

    logger.info("Importing API routes blueprint...")
    from .routes.api_routes import api_bp  # Import the API blueprint

    logger.info("Importing context overflow API...")
    from .routes.context_overflow_api import (
        context_overflow_bp,
    )  # Import context overflow API

    logger.info("Importing history routes...")
    from .routes.history_routes import history_bp

    logger.info("Importing metrics routes...")
    from .routes.metrics_routes import metrics_bp

    logger.info("Importing research routes...")
    from .routes.research_routes import research_bp

    logger.info("Importing settings routes...")
    from .routes.settings_routes import settings_bp

    logger.info("All core blueprints imported successfully")

    # Add root route
    @app.route("/")
    def index():
        """Root route - redirect to login if not authenticated"""
        from flask import redirect, session, url_for

        from ..constants import get_available_strategies
        from ..database.session_context import get_user_db_session
        from ..utilities.db_utils import get_settings_manager
        from .utils.templates import render_template_with_defaults

        # Check if user is authenticated
        if "username" not in session:
            return redirect(url_for("auth.login"))

        # Load current settings from database using proper session context
        username = session.get("username")
        settings = {}
        with get_user_db_session(username) as db_session:
            if db_session:
                settings_manager = get_settings_manager(db_session, username)
                settings = {
                    "llm_provider": settings_manager.get_setting(
                        "llm.provider", "ollama"
                    ),
                    "llm_model": settings_manager.get_setting("llm.model", ""),
                    "llm_openai_endpoint_url": settings_manager.get_setting(
                        "llm.openai_endpoint.url", ""
                    ),
                    "llm_ollama_url": settings_manager.get_setting(
                        "llm.ollama.url"
                    ),
                    "llm_lmstudio_url": settings_manager.get_setting(
                        "llm.lmstudio.url"
                    ),
                    "llm_local_context_window_size": settings_manager.get_setting(
                        "llm.local_context_window_size"
                    ),
                    "search_tool": settings_manager.get_setting(
                        "search.tool", ""
                    ),
                    "search_iterations": settings_manager.get_setting(
                        "search.iterations", 3
                    ),
                    "search_questions_per_iteration": settings_manager.get_setting(
                        "search.questions_per_iteration", 2
                    ),
                    "search_strategy": settings_manager.get_setting(
                        "search.search_strategy", "source-based"
                    ),
                    # Egress policy controls (Stage 1c UI).
                    "policy_egress_scope": settings_manager.get_setting(
                        "policy.egress_scope", DEFAULT_EGRESS_SCOPE
                    ),
                    "llm_require_local_endpoint": settings_manager.get_setting(
                        "llm.require_local_endpoint", False
                    ),
                    "embeddings_require_local": settings_manager.get_setting(
                        "embeddings.require_local", False
                    ),
                }

        # Debug logging
        log_settings(settings, "Research page settings loaded")

        return render_template_with_defaults(
            "pages/research.html",
            settings=settings,
            strategies=get_available_strategies(),
        )

    # Register auth blueprint FIRST (so login page is accessible)
    app.register_blueprint(auth_bp)  # Already has url_prefix="/auth"

    # Register other blueprints
    app.register_blueprint(research_bp)
    app.register_blueprint(history_bp)  # Already has url_prefix="/history"
    app.register_blueprint(metrics_bp)
    app.register_blueprint(settings_bp)  # Already has url_prefix="/settings"
    app.register_blueprint(
        api_bp, url_prefix="/research/api"
    )  # Register API blueprint with prefix
    app.register_blueprint(benchmark_bp)  # Register benchmark blueprint
    app.register_blueprint(
        context_overflow_bp, url_prefix="/metrics"
    )  # Register context overflow API

    # Register news API routes
    from .routes import news_routes

    app.register_blueprint(news_routes.bp)
    logger.info("News API routes registered successfully")

    # Register chat routes
    from ..chat.routes import chat_bp

    app.register_blueprint(chat_bp)
    logger.info("Chat routes registered successfully")

    # Register follow-up research routes
    from ..followup_research.routes import followup_bp

    app.register_blueprint(followup_bp)
    logger.info("Follow-up research routes registered successfully")

    # Register news page blueprint
    from ..news.web import create_news_blueprint

    news_bp = create_news_blueprint()
    app.register_blueprint(news_bp, url_prefix="/news")
    logger.info("News page routes registered successfully")

    # Register API v1 blueprint
    app.register_blueprint(api_blueprint)  # Already has url_prefix='/api/v1'

    # Register Research Library blueprint
    from ..research_library import library_bp, rag_bp, delete_bp

    app.register_blueprint(library_bp)  # Already has url_prefix='/library'
    logger.info("Research Library routes registered successfully")

    # Register RAG Management blueprint
    app.register_blueprint(rag_bp)  # Already has url_prefix='/library'
    logger.info("RAG Management routes registered successfully")

    # Register Deletion Management blueprint
    app.register_blueprint(delete_bp)  # Already has url_prefix='/library/api'
    logger.info("Deletion Management routes registered successfully")

    # Register Semantic Search blueprint
    from ..research_library.search import search_bp

    app.register_blueprint(search_bp)  # url_prefix='/library'
    logger.info("Semantic Search routes registered successfully")

    # Register Document Scheduler blueprint
    from ..research_scheduler.routes import scheduler_bp

    app.register_blueprint(scheduler_bp)
    logger.info("Document Scheduler routes registered successfully")

    # CSRF exemptions — Flask-WTF requires Blueprint objects (not strings)
    # to populate _exempt_blueprints.  Passing strings only populates
    # _exempt_views, which compares against module-qualified names and
    # silently fails to match Flask endpoint names.
    if hasattr(app, "extensions") and "csrf" in app.extensions:
        csrf = app.extensions["csrf"]
        # Only api_v1 is exempt: it's a programmatic REST API used by
        # external clients.  The api, benchmark, and research blueprints
        # are browser-facing and the frontend already sends CSRF tokens.
        for bp_name in ("api_v1",):
            bp_obj = app.blueprints.get(bp_name)
            if bp_obj is not None:
                csrf.exempt(bp_obj)

    # Add favicon route
    # Exempt favicon from rate limiting
    @app.route("/favicon.ico")
    @limiter.exempt
    def favicon():
        static_dir = app.config.get("STATIC_DIR", "static")
        return send_from_directory(
            static_dir, "favicon.ico", mimetype="image/x-icon"
        )

    # Add static route at the app level for compatibility
    # Exempt static files from rate limiting
    import re

    _HASHED_FILENAME_RE = re.compile(r"\.[A-Za-z0-9_-]{8,}\.")

    @app.route("/static/<path:path>")
    @limiter.exempt
    def app_serve_static(path):
        from ..security.path_validator import PathValidator

        static_dir = Path(app.config.get("STATIC_DIR", "static"))

        # First try to serve from dist directory (for built assets).
        # Flask captures path as "dist/js/app.abc.js", so strip the
        # "dist/" prefix before joining with dist_dir to avoid a
        # double-dist path (static/dist/dist/...).
        dist_prefix = "dist/"
        dist_dir = static_dir / "dist"
        if path.startswith(dist_prefix):
            dist_relative = path[len(dist_prefix) :]
            try:
                validated_path = PathValidator.validate_safe_path(
                    dist_relative,
                    dist_dir,
                    allow_absolute=False,
                    required_extensions=None,
                )

                if validated_path and validated_path.exists():
                    response = make_response(
                        send_from_directory(str(dist_dir), dist_relative)
                    )
                    if _HASHED_FILENAME_RE.search(dist_relative):
                        # Content-hashed files are safe for immutable caching
                        response.headers["Cache-Control"] = (
                            "public, max-age=31536000, immutable"
                        )
                    else:
                        response.headers["Cache-Control"] = (
                            "public, max-age=0, must-revalidate"
                        )
                    return response
            except ValueError:
                pass

        # Fall back to dist directory for Vite-built assets (fonts, etc.)
        # Vite uses base: '/static/' so CSS references /static/fonts/...
        # but the files live in static/dist/fonts/...
        try:
            validated_path = PathValidator.validate_safe_path(
                path, dist_dir, allow_absolute=False, required_extensions=None
            )

            if validated_path and validated_path.exists():
                response = make_response(
                    send_from_directory(str(dist_dir), path)
                )
                if _HASHED_FILENAME_RE.search(path):
                    response.headers["Cache-Control"] = (
                        "public, max-age=31536000, immutable"
                    )
                else:
                    response.headers["Cache-Control"] = (
                        "public, max-age=0, must-revalidate"
                    )
                return response
        except ValueError:
            pass

        # Fall back to regular static folder
        try:
            validated_path = PathValidator.validate_safe_path(
                path, static_dir, allow_absolute=False, required_extensions=None
            )

            if validated_path and validated_path.exists():
                response = make_response(
                    send_from_directory(str(static_dir), path)
                )
                # Non-hashed files must revalidate on each request
                response.headers["Cache-Control"] = (
                    "public, max-age=0, must-revalidate"
                )
                return response
        except ValueError:
            # Path validation failed
            pass

        abort(404)
        return None


def register_error_handlers(app):
    """Register error handlers with the Flask app."""
    from .auth.decorators import _is_api_path

    @app.errorhandler(404)
    def not_found(error):
        if _is_api_path(request.path):
            return make_response(jsonify({"error": "Not found"}), 404)
        return make_response("Not found", 404)

    @app.errorhandler(500)
    def server_error(error):
        if _is_api_path(request.path):
            return make_response(jsonify({"error": "Server error"}), 500)
        return make_response("Server error", 500)

    @app.errorhandler(401)
    def handle_unauthorized(error):
        if _is_api_path(request.path):
            return make_response(
                jsonify({"error": "Authentication required"}),
                401,
            )
        from .auth.decorators import _safe_redirect_to_login

        return _safe_redirect_to_login()

    @app.errorhandler(413)
    def handle_request_too_large(error):
        if _is_api_path(request.path):
            return make_response(
                jsonify({"error": "Request too large"}),
                413,
            )
        return make_response("Request too large", 413)

    from .exceptions import WebAPIException

    @app.errorhandler(WebAPIException)
    def handle_web_api_exception(error):
        """Handle WebAPIException and return JSON."""
        logger.error(
            "Web API error: {} (status {})", error.error_code, error.status_code
        )
        return jsonify(error.to_dict()), error.status_code

    # PolicyDeniedError can escape any synchronous request-path PEP
    # (LLM, embeddings, URL fetch) that wasn't caught at the call site.
    # Without this handler it surfaces as a 500 — turn it into a clean
    # 400 with the decision reason so the user (and the operator
    # audit log) sees a policy-shaped error instead of a stack trace.
    try:
        from ..security.egress.policy import PolicyDeniedError

        @app.errorhandler(PolicyDeniedError)
        def handle_policy_denied(error):
            reason = getattr(
                getattr(error, "decision", None), "reason", "denied"
            )
            target = getattr(error, "target", "")
            logger.bind(policy_audit=True).warning(
                "PolicyDeniedError surfaced at Flask error handler",
                reason=reason,
                target=target,
            )
            return make_response(
                jsonify(
                    {
                        "status": "error",
                        "message": (
                            f"Egress policy refused this request: {reason}"
                        ),
                    }
                ),
                400,
            )
    except ImportError:
        # egress_policy is in-tree; skipping the handler is only useful
        # for stripped-down test harnesses.
        pass

    # Handle CSRF validation errors as JSON
    try:
        from flask_wtf.csrf import CSRFError

        @app.errorhandler(CSRFError)
        def handle_csrf_error(error):
            return make_response(
                jsonify({"error": str(error.description)}), 400
            )
    except ImportError:
        pass

    # Handle News API exceptions globally
    try:
        from ..news.exceptions import NewsAPIException

        @app.errorhandler(NewsAPIException)
        def handle_news_api_exception(error):
            """Handle NewsAPIException and convert to JSON response."""
            from loguru import logger

            logger.error(
                "News API error: {} (status {})",
                error.error_code,
                error.status_code,
            )
            return jsonify(error.to_dict()), error.status_code
    except ImportError:
        # News module not available
        pass


def create_database(app):
    """
    DEPRECATED: Database creation is now handled per-user via encrypted_db.py
    This function is kept for compatibility but does nothing.
    """
    pass
