# import logging - replaced with loguru
import ipaddress
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.rate_limiter import limiter
from ..security.file_upload_validator import FileUploadValidator

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


def _is_private_ip(ip_str: str) -> bool:
    """Check if IP is a private/local network address (RFC 1918 + localhost).

    This allows LAN access over HTTP without requiring HTTPS, matching the
    behavior of other self-hosted applications like Jellyfin and Home Assistant.

    Private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, plus localhost.
    """
    try:
        ip = ipaddress.ip_address(ip_str)
        return ip.is_private or ip.is_loopback
    except ValueError:
        return False


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: With 200 files × 50MB limit, the default
    behavior could consume 10GB+ of memory per request.
    """

    # 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

    # Add proxy support for deployments behind load balancers/reverse proxies
    # This ensures X-Forwarded-For and X-Forwarded-Proto headers are properly handled
    # Important for rate limiting and security (gets real client IP, not proxy IP)
    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)
    )

    # WSGI middleware for dynamic cookie security
    # This wraps AFTER ProxyFix so we have access to the real client IP
    # Must be WSGI level because Flask session cookies are set after after_request handlers
    class SecureCookieMiddleware:
        """WSGI middleware to add Secure flag to cookies based on request context.

        Security model:
        - Localhost HTTP (127.0.0.1, ::1): Skip Secure flag (local traffic is safe)
        - Proxied requests (X-Forwarded-For present): Add Secure flag (production)
        - Non-localhost HTTP: Add Secure flag (will fail, by design - use HTTPS)
        - TESTING mode: Never add Secure flag (for CI/development)

        This prevents X-Forwarded-For spoofing attacks by checking for the header's
        presence rather than its value - if the header exists, we're behind a proxy.
        """

        def __init__(self, wsgi_app, flask_app):
            self.wsgi_app = wsgi_app
            self.flask_app = flask_app

        def __call__(self, environ, start_response):
            # Check if we should add Secure flag
            should_add_secure = self._should_add_secure_flag(environ)

            def custom_start_response(status, headers, exc_info=None):
                if should_add_secure:
                    # Modify Set-Cookie headers to add Secure flag
                    new_headers = []
                    for name, value in headers:
                        if name.lower() == "set-cookie":
                            if (
                                "; Secure" not in value
                                and "; secure" not in value
                            ):
                                value = value + "; Secure"
                        new_headers.append((name, value))
                    headers = new_headers
                return start_response(status, headers, exc_info)

            return self.wsgi_app(environ, custom_start_response)

        def _should_add_secure_flag(self, environ):
            """Determine if Secure flag should be added based on request context.

            Security model:
            - Check the ACTUAL connection IP (REMOTE_ADDR), not X-Forwarded-For header
            - SecureCookieMiddleware is outer wrapper, so we see original REMOTE_ADDR
            - If connection comes from private IP (client or proxy), allow HTTP
            - If connection comes from public IP, require HTTPS

            This is safe because:
            - We never trust X-Forwarded-For header values (can be spoofed)
            - We only check the actual TCP connection source IP
            - Spoofing X-Forwarded-For from public IP doesn't bypass this check
            - Local proxies (nginx on localhost/LAN) have private REMOTE_ADDR
            """
            # Skip if in explicit testing mode
            if self.flask_app.config.get("LDR_TESTING_MODE"):
                return False

            # Check actual connection source IP (before ProxyFix modifies it)
            # This is either:
            # - Direct client IP (if no proxy)
            # - Proxy server IP (if behind proxy)
            # Local proxies (nginx on localhost, Traefik on LAN) have private IPs
            remote_addr = environ.get("REMOTE_ADDR", "")
            is_private = _is_private_ip(remote_addr)

            # Check if HTTPS
            is_https = environ.get("wsgi.url_scheme") == "https"

            # Add Secure flag if:
            # - Using HTTPS (always secure over HTTPS)
            # - OR connection is from public IP (require HTTPS for public access)
            return is_https or not is_private

    # Wrap the app with our cookie security middleware
    app.wsgi_app = SecureCookieMiddleware(app.wsgi_app, app)  # type: ignore[method-assign]

    # WSGI middleware to remove Server header
    # This must be the outermost wrapper to catch headers added by Werkzeug
    class ServerHeaderMiddleware:
        """WSGI middleware to remove Server header from all responses.

        Prevents information disclosure about the underlying web server.
        Must be outermost middleware to catch headers added by WSGI layer.
        """

        def __init__(self, wsgi_app):
            self.wsgi_app = wsgi_app

        def __call__(self, environ, start_response):
            def custom_start_response(status, headers, exc_info=None):
                filtered_headers = [
                    (name, value)
                    for name, value in headers
                    if name.lower() != "server"
                ]
                return start_response(status, filtered_headers, exc_info)

            return self.wsgi_app(environ, custom_start_response)

    # Apply ServerHeaderMiddleware as outermost wrapper
    app.wsgi_app = ServerHeaderMiddleware(app.wsgi_app)  # type: ignore[method-assign]

    # 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") 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 ..news.subscription_manager.scheduler import (
                get_news_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_news_scheduler()
            scheduler.initialize_with_settings(settings_manager)
            scheduler.start()
            app.news_scheduler = scheduler  # type: ignore[attr-defined]
            logger.info("News scheduler started with activity-based tracking")
        else:
            # Don't initialize scheduler if disabled
            app.news_scheduler = None  # type: ignore[attr-defined]
            logger.info("News scheduler disabled - not initializing")
    except Exception:
        logger.exception("Failed to initialize news scheduler")
        app.news_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")

    # Register shutdown cleanup for thread engine disposal
    import atexit

    atexit.register(db_manager.cleanup_all_thread_engines)

    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")

    # Flush any queued logs from background threads
    logger.info("Importing log_utils...")
    from ..utilities.log_utils import flush_log_queue

    app.before_request(flush_log_queue)
    logger.info("Log flushing middleware registered")

    # 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 ResearchStatus

    @app.context_processor
    def inject_frontend_constants():
        terminal = [
            ResearchStatus.COMPLETED,
            ResearchStatus.SUSPENDED,
            ResearchStatus.FAILED,
            ResearchStatus.ERROR,
            ResearchStatus.CANCELLED,
        ]
        return {
            "research_status_enum": {m.name: m.value for m in ResearchStatus},
            "research_terminal_states": [str(s) for s in terminal],
        }

    # 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")

        # Dead thread sweep — rate-limited internally to every 60 seconds.
        # See encrypted_db.cleanup_dead_thread_engines() for detailed
        # rationale. 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 = {}
        show_all = False
        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"
                    ),
                }
                show_all = settings_manager.get_setting(
                    "search.show_all_strategies", False
                )

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

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

    # 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 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."""

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

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

    @app.errorhandler(401)
    def handle_unauthorized(error):
        if request.path.startswith("/api/") or request.path.startswith(
            "/settings/api/"
        ):
            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 request.path.startswith("/api/"):
            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: %s (status %d)", error.error_code, error.status_code
        )
        return jsonify(error.to_dict()), error.status_code

    # Handle CSRF validation errors with helpful message
    try:
        from flask_wtf.csrf import CSRFError

        @app.errorhandler(CSRFError)
        def handle_csrf_error(error):
            """Handle CSRF errors with helpful debugging info."""
            # Check if this might be a Secure cookie issue over HTTP
            is_http = not request.is_secure
            is_private = _is_private_ip(request.remote_addr or "")
            is_proxied = request.headers.get("X-Forwarded-For") is not None

            error_msg = str(error.description)

            # Provide detailed help for HTTP + public IP or proxied scenario
            if is_http and (not is_private or is_proxied):
                logger.warning(
                    f"CSRF validation failed - likely due to Secure cookie over HTTP. "
                    f"remote_addr={request.remote_addr}, proxied={is_proxied}, "
                    f"host={request.host}"
                )
                error_msg = (
                    "Session cookie error: You're accessing over HTTP from a "
                    "public IP address or through a proxy. "
                    "This is blocked for security reasons.\n\n"
                    "Solutions:\n"
                    "1. Use HTTPS with a reverse proxy (recommended for production)\n"
                    "2. Access from your local network (LAN IPs like 192.168.x.x work over HTTP)\n"
                    "3. Access directly from localhost (http://127.0.0.1:5000)\n"
                    "4. Use SSH tunnel: ssh -L 5000:localhost:5000 user@server, "
                    "then access http://localhost:5000\n\n"
                    "Note: LAN access (192.168.x.x, 10.x.x.x, 172.16-31.x.x) works over HTTP. "
                    "Only public internet access requires HTTPS."
                )

            return make_response(jsonify({"error": error_msg}), 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: %s (status %d)",
                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
