"""
Authentication routes for login, register, and logout.
Uses SQLCipher encrypted databases with browser password manager support.
"""

import threading
from datetime import datetime, timezone, UTC

from flask import (
    Blueprint,
    flash,
    jsonify,
    redirect,
    render_template,
    request,
    session,
    url_for,
)
from loguru import logger

from ...database.auth_db import get_auth_db_session
from ...database.encrypted_db import db_manager
from ...database.models.auth import User
from sqlalchemy.exc import IntegrityError
from ...utilities.threading_utils import thread_context, thread_with_app_context
from .session_manager import SessionManager
from ..server_config import load_server_config
from ..utils.rate_limiter import login_limit, registration_limit
from ...security.url_validator import URLValidator

auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
session_manager = SessionManager()


@auth_bp.route("/csrf-token", methods=["GET"])
def get_csrf_token():
    """
    Get CSRF token for API requests.
    Returns the current CSRF token for the session.
    This endpoint makes it easy for API clients to get the CSRF token
    programmatically without parsing HTML.
    """
    from flask_wtf.csrf import generate_csrf

    # Generate or get existing CSRF token for this session
    token = generate_csrf()

    return jsonify({"csrf_token": token}), 200


@auth_bp.route("/login", methods=["GET"])
def login_page():
    """
    Login page (GET only).
    Not rate limited - viewing the page should always work.
    """
    config = load_server_config()
    # Check if already logged in
    if session.get("username"):
        return redirect(url_for("index"))

    # Preserve the next parameter for post-login redirect
    next_page = request.args.get("next", "")

    return render_template(
        "auth/login.html",
        has_encryption=db_manager.has_encryption,
        allow_registrations=config.get("allow_registrations", True),
        next_page=next_page,
    )


@auth_bp.route("/login", methods=["POST"])
@login_limit
def login():
    """
    Login handler (POST only).
    Rate limited to 5 attempts per 15 minutes per IP to prevent brute force attacks.
    """
    config = load_server_config()
    # POST - Handle login
    username = request.form.get("username", "").strip()
    password = request.form.get("password", "")
    remember = request.form.get("remember", "false") == "true"

    if not username or not password:
        flash("Username and password are required", "error")
        return render_template(
            "auth/login.html",
            has_encryption=db_manager.has_encryption,
            allow_registrations=config.get("allow_registrations", True),
        ), 400

    # Try to open user's encrypted database
    engine = db_manager.open_user_database(username, password)

    if engine is None:
        # Invalid credentials or database doesn't exist
        logger.warning(f"Failed login attempt for username: {username}")
        flash("Invalid username or password", "error")
        return render_template(
            "auth/login.html",
            has_encryption=db_manager.has_encryption,
            allow_registrations=config.get("allow_registrations", True),
        ), 401

    # Success! Create session
    session_id = session_manager.create_session(username, remember)
    session["session_id"] = session_id
    session["username"] = username
    session.permanent = remember

    # Store password temporarily for post-login database access
    from ...database.temp_auth import temp_auth_store

    auth_token = temp_auth_store.store_auth(username, password)
    session["temp_auth_token"] = auth_token

    # Also store in session password store for metrics access
    from ...database.session_passwords import session_password_store

    session_password_store.store_session_password(
        username, session_id, password
    )

    logger.info(f"User {username} logged in successfully")

    # Defer non-critical post-login work to a background thread so the
    # redirect returns immediately (settings migration, library init,
    # news scheduler, and model cache clearing are all idempotent and
    # can safely run after the response).
    app_ctx = thread_context()
    thread = threading.Thread(
        target=thread_with_app_context(_perform_post_login_tasks),
        args=(app_ctx, username, password),
        daemon=True,
    )
    thread.start()

    # Redirect to original requested page or dashboard
    next_page = request.args.get("next", "")

    # Security: URLValidator.is_safe_redirect_url() validates that the target URL
    # has http/https scheme and netloc matches the application host, preventing
    # open redirect attacks (CWE-601). CodeQL doesn't recognize custom sanitizers.
    if next_page and URLValidator.is_safe_redirect_url(
        next_page, request.host_url
    ):
        return redirect(next_page)  # codeql[py/url-redirection]

    return redirect(url_for("index"))


def _perform_post_login_tasks(username: str, password: str) -> None:
    """Run non-critical post-login operations in a background thread.

    Each operation is wrapped in its own try/except so that one failure
    does not prevent the others from running. All operations here are
    idempotent and safe to retry on the next login.
    """
    # 1. Settings version check + migration
    try:
        from ...settings.manager import SettingsManager

        db_session = db_manager.get_session(username)
        try:
            if db_session:
                settings_manager = SettingsManager(db_session)
                if not settings_manager.db_version_matches_package():
                    logger.info(
                        f"Database version mismatch for {username} "
                        "- loading missing default settings"
                    )
                    settings_manager.load_from_defaults_file(
                        commit=True, overwrite=False
                    )
                    settings_manager.update_db_version()
                    logger.info(
                        f"Missing default settings loaded and version "
                        f"updated for user {username}"
                    )
        finally:
            if db_session:
                db_session.close()
    except Exception:
        logger.exception(f"Post-login settings migration failed for {username}")

    # 2. Initialize library system (source types and default collection)
    try:
        from ...database.library_init import initialize_library_for_user

        init_results = initialize_library_for_user(username, password)
        if init_results.get("success"):
            logger.info(f"Library system initialized for user {username}")
        else:
            logger.warning(
                f"Library initialization issue for {username}: "
                f"{init_results.get('error', 'Unknown error')}"
            )
    except Exception:
        logger.exception(f"Post-login library init failed for {username}")

    # 3. Update last_login in auth DB + notify news scheduler
    try:
        auth_db = get_auth_db_session()
        try:
            user = auth_db.query(User).filter_by(username=username).first()
            if user:
                user.last_login = datetime.now(UTC)

            try:
                from ...news.subscription_manager.scheduler import (
                    get_news_scheduler,
                )

                scheduler = get_news_scheduler()
                if scheduler.is_running:
                    scheduler.update_user_info(username, password)
                    logger.info(
                        f"Updated scheduler with user info for {username}"
                    )
            except Exception:
                logger.exception("Could not update scheduler on login")

            auth_db.commit()
        finally:
            auth_db.close()
    except Exception:
        logger.exception(f"Post-login auth DB update failed for {username}")

    # 4. Clear the model cache to ensure fresh provider data
    try:
        from ...database.models import ProviderModel

        user_db_session = db_manager.get_session(username)
        try:
            if user_db_session:
                deleted_count = user_db_session.query(ProviderModel).delete()
                user_db_session.commit()
                logger.info(
                    f"Cleared {deleted_count} cached models "
                    f"for user {username} on login"
                )
        finally:
            if user_db_session:
                user_db_session.close()
    except Exception:
        logger.exception(f"Post-login model cache clear failed for {username}")

    logger.info(f"Post-login tasks completed for user {username}")


@auth_bp.route("/register", methods=["GET"])
def register_page():
    """
    Registration page (GET only).
    Not rate limited - viewing the page should always work.
    """
    config = load_server_config()
    if not config.get("allow_registrations", True):
        flash("New user registrations are currently disabled.", "error")
        return redirect(url_for("auth.login_page"))

    return render_template(
        "auth/register.html", has_encryption=db_manager.has_encryption
    )


@auth_bp.route("/register", methods=["POST"])
@registration_limit
def register():
    """
    Registration handler (POST only).
    Creates new encrypted database for user with clear warnings about password recovery.
    Rate limited to 3 attempts per hour per IP to prevent registration spam.
    """
    config = load_server_config()
    if not config.get("allow_registrations", True):
        flash("New user registrations are currently disabled.", "error")
        return redirect(url_for("auth.login_page"))

    # POST - Handle registration
    username = request.form.get("username", "").strip()
    password = request.form.get("password", "")
    confirm_password = request.form.get("confirm_password", "")
    acknowledge = request.form.get("acknowledge", "false") == "true"

    # Validation
    errors = []

    if not username:
        errors.append("Username is required")
    elif len(username) < 3:
        errors.append("Username must be at least 3 characters")
    elif not username.replace("_", "").replace("-", "").isalnum():
        errors.append(
            "Username can only contain letters, numbers, underscores, and hyphens"
        )

    if not password:
        errors.append("Password is required")
    elif len(password) < 8:
        errors.append("Password must be at least 8 characters")

    if password != confirm_password:
        errors.append("Passwords do not match")

    if not acknowledge:
        errors.append(
            "You must acknowledge that password recovery is not possible"
        )

    # Check if user already exists
    # Use generic error message to prevent account enumeration
    # Note: While this creates a minor timing difference, it's acceptable because:
    # 1. Rate limiting prevents automated timing analysis
    # 2. Generic error message prevents content-based enumeration
    # 3. Local database query timing is minimal (no network calls)
    # 4. Better UX with immediate feedback outweighs minor timing risk
    # See: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
    if username and db_manager.user_exists(username):
        errors.append("Registration failed. Please try a different username.")

    if errors:
        for error in errors:
            flash(error, "error")
        return render_template(
            "auth/register.html", has_encryption=db_manager.has_encryption
        ), 400

    # Create user in auth database
    auth_db = get_auth_db_session()
    try:
        new_user = User(username=username)
        auth_db.add(new_user)
        auth_db.commit()
    except IntegrityError:
        # Catch duplicate username specifically (race condition case)
        # This handles the edge case where two requests for the same username
        # pass the user_exists() check simultaneously
        logger.warning(f"Duplicate username attempted: {username}")
        auth_db.rollback()
        auth_db.close()
        flash("Registration failed. Please try a different username.", "error")
        return render_template(
            "auth/register.html", has_encryption=db_manager.has_encryption
        ), 400
    except Exception:
        logger.exception(f"Registration failed for {username}")
        auth_db.rollback()
        auth_db.close()
        flash("Registration failed. Please try again.", "error")
        return render_template(
            "auth/register.html", has_encryption=db_manager.has_encryption
        ), 500
    finally:
        # Note: In success case, we've already committed; in exception case,
        # we rollback and close above, but finally ensures close if commit succeeded
        try:
            auth_db.close()
        except Exception:
            pass  # Already closed in exception handler

    try:
        # Create encrypted database for user
        db_manager.create_user_database(username, password)

        # Auto-login after registration
        session_id = session_manager.create_session(username, False)
        session["session_id"] = session_id
        session["username"] = username

        # Store password temporarily for post-registration database access
        from ...database.temp_auth import temp_auth_store

        auth_token = temp_auth_store.store_auth(username, password)
        session["temp_auth_token"] = auth_token

        # Also store in session password store for metrics access
        from ...database.session_passwords import session_password_store

        session_password_store.store_session_password(
            username, session_id, password
        )

        # Notify the news scheduler about the new user
        try:
            from ...news.subscription_manager.scheduler import (
                get_news_scheduler,
            )

            scheduler = get_news_scheduler()
            if scheduler.is_running:
                scheduler.update_user_info(username, password)
                logger.info(
                    f"Updated scheduler with new user info for {username}"
                )
        except Exception:
            logger.exception("Could not update scheduler on registration")

        logger.info(f"New user registered: {username}")

        # Initialize library system (source types and default collection)
        from ...database.library_init import initialize_library_for_user

        try:
            init_results = initialize_library_for_user(username, password)
            if init_results.get("success"):
                logger.info(
                    f"Library system initialized for new user {username}"
                )
            else:
                logger.warning(
                    f"Library initialization issue for {username}: {init_results.get('error', 'Unknown error')}"
                )
        except Exception:
            logger.exception(
                f"Error initializing library for new user {username}"
            )
            # Don't block registration on library init failure

        return redirect(url_for("index"))

    except Exception:
        logger.exception(f"Registration failed for {username}")
        flash("Registration failed. Please try again.", "error")
        return render_template(
            "auth/register.html", has_encryption=db_manager.has_encryption
        ), 500


@auth_bp.route("/logout", methods=["GET", "POST"])
def logout():
    """
    Logout handler.
    Clears session and closes database connections.
    Supports both GET (for direct navigation) and POST (for form submission).
    """
    username = session.get("username")
    session_id = session.get("session_id")

    if username:
        # Close database connection
        db_manager.close_user_database(username)

        # Clear session
        if session_id:
            session_manager.destroy_session(session_id)

            # Clear session password
            from ...database.session_passwords import session_password_store

            session_password_store.clear_session(username, session_id)

        session.clear()

        logger.info(f"User {username} logged out")
        flash("You have been logged out successfully", "info")

    return redirect(url_for("auth.login"))


@auth_bp.route("/check", methods=["GET"])
def check_auth():
    """
    Check if user is authenticated (for AJAX requests).
    """
    if session.get("username"):
        return jsonify({"authenticated": True, "username": session["username"]})
    else:
        return jsonify({"authenticated": False}), 401


@auth_bp.route("/change-password", methods=["GET", "POST"])
def change_password():
    """
    Change password for current user.
    Requires current password and re-encrypts database.
    """
    username = session.get("username")
    if not username:
        return redirect(url_for("auth.login"))

    if request.method == "GET":
        return render_template("auth/change_password.html")

    # POST - Handle password change
    current_password = request.form.get("current_password", "")
    new_password = request.form.get("new_password", "")
    confirm_password = request.form.get("confirm_password", "")

    # Validation
    errors = []

    if not current_password:
        errors.append("Current password is required")

    if not new_password:
        errors.append("New password is required")
    elif len(new_password) < 8:
        errors.append("New password must be at least 8 characters")

    if new_password != confirm_password:
        errors.append("New passwords do not match")

    if current_password == new_password:
        errors.append("New password must be different from current password")

    if errors:
        for error in errors:
            flash(error, "error")
        return render_template("auth/change_password.html"), 400

    # Attempt password change
    success = db_manager.change_password(
        username, current_password, new_password
    )

    if success:
        # The rekey is the ONLY step needed.  The auth database stores no
        # password hash — login works by attempting to decrypt the user's
        # SQLCipher database.  Do NOT add an auth-DB password-hash update
        # here; it would fail (User model has no set_password method) and
        # is architecturally unnecessary.
        session.clear()

        logger.info(f"Password changed for user {username}")
        flash(
            "Password changed successfully. Please login with your new password.",
            "success",
        )
        return redirect(url_for("auth.login"))
    else:
        flash("Current password is incorrect", "error")
        return render_template("auth/change_password.html"), 401


@auth_bp.route("/integrity-check", methods=["GET"])
def integrity_check():
    """
    Check database integrity for current user.
    """
    username = session.get("username")
    if not username:
        return jsonify({"error": "Not authenticated"}), 401

    is_valid = db_manager.check_database_integrity(username)

    return jsonify(
        {
            "username": username,
            "integrity": "valid" if is_valid else "corrupted",
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }
    )
