"""Authentication, durable lockout, database identity and session governance."""

from __future__ import annotations

from datetime import datetime
from functools import wraps
import hmac
import os

from flask import (
    current_app,
    flash,
    g,
    has_request_context,
    jsonify,
    redirect,
    render_template,
    request,
    session,
    url_for,
)
from werkzeug.routing import BuildError
from werkzeug.security import check_password_hash

from config import LOGIN_PASSWORD
from database.user_models import LoginHistory
from services.auth_identity_service import (
    KNOWN_ROLES,
    auth_auto_cutover_min_successes,
    database_auth_can_authorize,
    database_auth_is_evaluated,
    identity_version,
    legacy_auth_can_authorize,
    resolve_auth_identity_mode,
    resolve_effective_auth_identity_mode,
    resolve_client_ip,
    role_is_allowed,
)


def _bounded_int(name: str, default: int, minimum: int, maximum: int) -> int:
    try:
        value = int(os.getenv(name, str(default)))
    except (TypeError, ValueError):
        value = default
    return max(minimum, min(value, maximum))


_DISABLE_LOGIN_REQUESTED = os.getenv("DISABLE_LOGIN", "false").lower() == "true"
_ALLOW_INSECURE_LOGIN_BYPASS = (
    os.getenv("MOMO_ALLOW_INSECURE_CONFIG_FOR_TESTS", "false").lower() == "true"
)
DISABLE_LOGIN = _DISABLE_LOGIN_REQUESTED and _ALLOW_INSECURE_LOGIN_BYPASS

MAX_LOGIN_ATTEMPTS = _bounded_int("MOMO_AUTH_MAX_ATTEMPTS", 5, 3, 20)
LOCKOUT_DURATION = _bounded_int("MOMO_AUTH_LOCKOUT_SECONDS", 300, 30, 86400)
ATTEMPT_RESET_TIME = _bounded_int("MOMO_AUTH_ATTEMPT_WINDOW_SECONDS", 1800, 60, 86400)

if DISABLE_LOGIN:
    print("[Security] 登入驗證僅於明確測試模式停用")
elif _DISABLE_LOGIN_REQUESTED:
    print("[Security] 已拒絕 DISABLE_LOGIN：僅測試環境可使用登入繞過")


def get_identity_session():
    """Late-bind the database session so tests and workers share one seam."""
    from database.manager import get_session

    return get_session()


def get_client_ip() -> str:
    return resolve_client_ip(
        request.remote_addr,
        request.headers.get("X-Forwarded-For"),
        request.headers.get("X-Real-IP"),
    )


def _user_agent() -> str:
    return (request.headers.get("User-Agent") or "")[:256]


def _lockout_status(username: str | None, client_ip: str) -> tuple[bool, int, int]:
    from services.user_service import UserService

    db_session = get_identity_session()
    try:
        return UserService(db_session).get_lockout_status(
            username=username,
            ip_address=client_ip,
            max_attempts=MAX_LOGIN_ATTEMPTS,
            reset_seconds=ATTEMPT_RESET_TIME,
            lockout_seconds=LOCKOUT_DURATION,
        )
    finally:
        db_session.close()


def is_ip_locked(ip: str) -> tuple[bool, int]:
    """Compatibility wrapper backed by durable login_history state."""
    locked, remaining, _ = _lockout_status(None, ip)
    return locked, remaining


def record_login_failure(ip: str) -> bool:
    """Compatibility wrapper; new login flow records username and audit detail."""
    from services.user_service import UserService

    db_session = get_identity_session()
    try:
        service = UserService(db_session)
        recorded = service.record_login(
            None,
            "legacy-shared",
            ip,
            _user_agent() if has_request_context() else None,
            LoginHistory.STATUS_FAILED,
            "invalid_credentials",
        )
        if recorded is None:
            return True
        locked, _, _ = service.get_lockout_status(
            username="legacy-shared",
            ip_address=ip,
            max_attempts=MAX_LOGIN_ATTEMPTS,
            reset_seconds=ATTEMPT_RESET_TIME,
            lockout_seconds=LOCKOUT_DURATION,
        )
        return locked
    finally:
        db_session.close()


def clear_login_attempts(_ip: str) -> None:
    """A durable success event supersedes older failures; no delete is required."""


def validate_password_strength(password):
    if not password:
        return False, "密碼不能為空"
    if len(password) < 8:
        return False, "密碼長度至少需要 8 個字元"
    if not any(c.isalpha() for c in password):
        return False, "密碼必須包含英文字母"
    if not any(c.isdigit() for c in password):
        return False, "密碼必須包含數字"
    return True, None


def _legacy_password_matches(input_password: str) -> bool:
    if not LOGIN_PASSWORD:
        return False
    if LOGIN_PASSWORD.startswith(("pbkdf2:", "scrypt:")):
        return check_password_hash(LOGIN_PASSWORD, input_password)
    return hmac.compare_digest(input_password, LOGIN_PASSWORD)


def _effective_auth_identity_mode(service, configured_mode: str) -> str:
    if configured_mode != "auto":
        return configured_mode
    cutover = service.get_auth_auto_cutover_state(
        min_successes=auth_auto_cutover_min_successes()
    )
    return resolve_effective_auth_identity_mode(
        configured_mode,
        auto_cutover_ready=cutover["ready"],
    )


def _audit_session_event(status: str, reason: str) -> None:
    """Best-effort audit for logout/denial; login itself remains fail-closed."""
    from services.user_service import UserService

    db_session = None
    try:
        db_session = get_identity_session()
        UserService(db_session).record_login(
            session.get("user_id"),
            session.get("username") or "legacy-shared",
            get_client_ip(),
            _user_agent(),
            status,
            reason,
        )
    except Exception as exc:
        print(f"[Security] audit event unavailable: {type(exc).__name__}")
    finally:
        if db_session is not None:
            db_session.close()


def _session_state(required_roles=()) -> dict:
    cached = getattr(g, "_ewoooc_auth_session_state", None)
    if cached is not None and (
        not required_roles or role_is_allowed(cached.get("role"), required_roles)
    ):
        return cached

    if DISABLE_LOGIN:
        state = {"ok": True, "reason": "test_bypass", "role": "admin"}
        g._ewoooc_auth_session_state = state
        return state
    if not session.get("logged_in"):
        return {"ok": False, "reason": "authentication_required", "role": None}

    try:
        configured_mode = resolve_auth_identity_mode()
    except ValueError:
        return {"ok": False, "reason": "identity_policy_invalid", "role": None}

    auth_source = session.get("auth_source")
    mode = configured_mode
    if configured_mode == "auto" and auth_source == "legacy_shared_password":
        from services.user_service import UserService

        db_session = None
        try:
            db_session = get_identity_session()
            mode = _effective_auth_identity_mode(
                UserService(db_session), configured_mode
            )
        except Exception as exc:
            print(f"[Security] auto cutover validation unavailable: {type(exc).__name__}")
            return {"ok": False, "reason": "identity_store_unavailable", "role": None}
        finally:
            if db_session is not None:
                db_session.close()
    role = session.get("role")
    if auth_source == "database":
        from services.user_service import UserService

        db_session = None
        revoke_reason = None
        try:
            db_session = get_identity_session()
            user = UserService(db_session).get_user_by_id(session.get("user_id"))
            if not user or not user.is_active or user.role not in KNOWN_ROLES:
                revoke_reason = "identity_inactive_or_missing"
            elif session.get("identity_version") != identity_version(user):
                revoke_reason = "identity_version_changed"
            else:
                role = user.role
                session["role"] = user.role
                session["username"] = user.username
        except Exception as exc:
            print(f"[Security] identity validation unavailable: {type(exc).__name__}")
            return {"ok": False, "reason": "identity_store_unavailable", "role": None}
        finally:
            if db_session is not None:
                db_session.close()
        if revoke_reason:
            _audit_session_event(LoginHistory.STATUS_REVOKED, revoke_reason)
            session.clear()
            return {"ok": False, "reason": "session_revoked", "role": None}
    elif auth_source == "legacy_shared_password":
        if not legacy_auth_can_authorize(mode) or role != "admin":
            session.clear()
            return {"ok": False, "reason": "legacy_session_revoked", "role": None}
    elif current_app.testing:
        # Legacy fixtures may exercise business routes without constructing a
        # production identity cookie. This branch is impossible in production.
        role = session.get("role") or "admin"
        if role not in KNOWN_ROLES:
            session.clear()
            return {"ok": False, "reason": "unversioned_session", "role": None}
    else:
        # Pre-policy cookies never inherit admin privileges from a missing field.
        session.clear()
        return {"ok": False, "reason": "unversioned_session", "role": None}

    if required_roles and not role_is_allowed(role, required_roles):
        _audit_session_event(LoginHistory.STATUS_DENIED, "role_not_allowed")
        return {"ok": False, "reason": "authorization_denied", "role": role}

    state = {"ok": True, "reason": "authenticated", "role": role}
    g._ewoooc_auth_session_state = state
    return state


def _is_api_request() -> bool:
    return request.path.startswith("/api/") or "/api/" in request.path


def _denied_response(state: dict):
    reason = state.get("reason") or "authentication_required"
    if reason == "identity_store_unavailable":
        if _is_api_request():
            return jsonify({"success": False, "error": reason}), 503
        return render_template("login.html", error="身分服務暫時無法使用，請稍後再試。"), 503
    if reason == "authorization_denied":
        if _is_api_request():
            return jsonify({"success": False, "error": reason}), 403
        flash("您沒有權限執行此操作", "danger")
        try:
            return redirect(url_for("dashboard.index"))
        except BuildError:
            return redirect(url_for("login"))
    if _is_api_request():
        return jsonify({
            "success": False,
            "error": "authentication_required",
            "message": "此端點需要登入。",
        }), 401
    return redirect(url_for("login"))


def login_required(f):
    @wraps(f)
    def decorated_view(*args, **kwargs):
        state = _session_state()
        if not state["ok"]:
            return _denied_response(state)
        return f(*args, **kwargs)

    return decorated_view


def require_authenticated_request(*required_roles):
    """Central policy hook with API/page aware denial semantics."""
    state = _session_state(required_roles)
    if state["ok"]:
        return None
    return _denied_response(state)


def internal_key_required(f):
    """Require the shared internal transport key for non-session service calls."""
    @wraps(f)
    def decorated_view(*args, **kwargs):
        expected = os.getenv("INTERNAL_API_KEY", "").strip()
        provided = request.headers.get("X-Internal-Key", "").strip()
        if not expected:
            return jsonify({"success": False, "error": "internal_auth_not_configured"}), 503
        if not provided or not hmac.compare_digest(provided, expected):
            return jsonify({"success": False, "error": "invalid_internal_key"}), 401
        return f(*args, **kwargs)

    return decorated_view


def get_current_user():
    state = _session_state()
    if not state["ok"]:
        return None
    return {
        "logged_in": True,
        "user_id": session.get("user_id"),
        "login_time": session.get("login_time"),
        "client_ip": session.get("client_ip"),
        "role": state.get("role"),
        "username": session.get("username"),
        "auth_source": session.get("auth_source"),
    }


def role_required(*roles):
    unknown = set(roles) - KNOWN_ROLES
    if unknown:
        raise ValueError(f"Unknown roles: {', '.join(sorted(unknown))}")

    def decorator(f):
        @wraps(f)
        def decorated_view(*args, **kwargs):
            state = _session_state(roles)
            if not state["ok"]:
                return _denied_response(state)
            return f(*args, **kwargs)

        return decorated_view

    return decorator


def admin_required(f):
    return role_required("admin")(f)


def _set_database_session(user, client_ip: str, password_change_required: bool) -> None:
    session.clear()
    session.permanent = True
    session.update({
        "logged_in": True,
        "login_time": datetime.now().isoformat(),
        "client_ip": client_ip,
        "user_id": user.id,
        "role": user.role,
        "username": user.username,
        "auth_source": "database",
        "identity_version": identity_version(user),
        "password_change_required": bool(password_change_required),
    })


def _set_legacy_session(client_ip: str) -> None:
    session.clear()
    session.permanent = True
    session.update({
        "logged_in": True,
        "login_time": datetime.now().isoformat(),
        "client_ip": client_ip,
        "role": "admin",
        "username": "legacy-admin",
        "auth_source": "legacy_shared_password",
    })


def init_auth_routes(app):
    """Register database-first login and audited logout routes."""

    @app.route("/login", methods=["GET", "POST"])
    def login():
        from services.user_service import UserService

        client_ip = get_client_ip()
        configured_mode = resolve_auth_identity_mode()
        mode = configured_mode
        username = (request.form.get("username") or "").strip()
        lockout_identity = username or "legacy-shared"

        db_session = None
        try:
            db_session = get_identity_session()
            service = UserService(db_session)
            mode = _effective_auth_identity_mode(service, configured_mode)
            locked, remaining_seconds, failure_count = service.get_lockout_status(
                username=lockout_identity,
                ip_address=client_ip,
                max_attempts=MAX_LOGIN_ATTEMPTS,
                reset_seconds=ATTEMPT_RESET_TIME,
                lockout_seconds=LOCKOUT_DURATION,
            )
        except Exception as exc:
            print(f"[Security] durable lockout unavailable: {type(exc).__name__}")
            if db_session is not None:
                db_session.close()
            return render_template(
                "login.html",
                error="身分服務暫時無法使用，請稍後再試。",
                auth_mode=mode,
                username=username,
            ), 503

        try:
            if locked:
                minutes, seconds = divmod(remaining_seconds, 60)
                error = f"登入嘗試已暫停，請在 {minutes} 分 {seconds} 秒後再試。"
                return render_template(
                    "login.html", error=error, auth_mode=mode, username=username
                ), 429

            if request.method == "GET":
                return render_template(
                    "login.html", error=None, auth_mode=mode, username=username
                )

            input_password = request.form.get("password") or ""
            if not input_password:
                return render_template(
                    "login.html",
                    error="請輸入密碼",
                    auth_mode=mode,
                    username=username,
                ), 400

            db_user = None
            password_change_required = False
            if username and database_auth_is_evaluated(mode):
                db_user, _, password_change_required = service.authenticate(
                    username, input_password
                )

            legacy_valid = (
                legacy_auth_can_authorize(mode)
                and _legacy_password_matches(input_password)
            )
            database_valid = bool(
                db_user and database_auth_can_authorize(mode)
            )

            if mode == "shadow" and username:
                shadow_event = service.record_login(
                    db_user.id if db_user else None,
                    username,
                    client_ip,
                    _user_agent(),
                    LoginHistory.STATUS_SHADOW,
                    "database_match" if db_user else "database_miss",
                )
                if shadow_event is None:
                    return render_template(
                        "login.html",
                        error="登入稽核暫時無法使用，請稍後再試。",
                        auth_mode=mode,
                        username=username,
                    ), 503

            if database_valid:
                history = service.record_login(
                    db_user.id,
                    db_user.username,
                    client_ip,
                    _user_agent(),
                    LoginHistory.STATUS_SUCCESS,
                    "auth_source=database",
                )
                if history is None:
                    return render_template(
                        "login.html",
                        error="登入稽核暫時無法使用，請稍後再試。",
                        auth_mode=mode,
                        username=username,
                    ), 503
                _set_database_session(db_user, client_ip, password_change_required)
                return redirect(url_for("dashboard.index"))

            if legacy_valid:
                history = service.record_login(
                    None,
                    "legacy-shared",
                    client_ip,
                    _user_agent(),
                    LoginHistory.STATUS_SUCCESS,
                    "auth_source=legacy_shared_password",
                )
                if history is None:
                    return render_template(
                        "login.html",
                        error="登入稽核暫時無法使用，請稍後再試。",
                        auth_mode=mode,
                        username=username,
                    ), 503
                _set_legacy_session(client_ip)
                return redirect(url_for("dashboard.index"))

            failure = service.record_login(
                db_user.id if db_user else None,
                lockout_identity,
                client_ip,
                _user_agent(),
                LoginHistory.STATUS_FAILED,
                "invalid_credentials",
            )
            if failure is None:
                return render_template(
                    "login.html",
                    error="登入稽核暫時無法使用，請稍後再試。",
                    auth_mode=mode,
                    username=username,
                ), 503

            now_locked, remaining_seconds, failure_count = service.get_lockout_status(
                username=lockout_identity,
                ip_address=client_ip,
                max_attempts=MAX_LOGIN_ATTEMPTS,
                reset_seconds=ATTEMPT_RESET_TIME,
                lockout_seconds=LOCKOUT_DURATION,
            )
            if now_locked:
                service.record_login(
                    None,
                    lockout_identity,
                    client_ip,
                    _user_agent(),
                    LoginHistory.STATUS_LOCKED,
                    "max_attempts_reached",
                )
                error = f"登入嘗試已暫停 {LOCKOUT_DURATION // 60} 分鐘。"
                return render_template(
                    "login.html", error=error, auth_mode=mode, username=username
                ), 429

            remaining_attempts = max(0, MAX_LOGIN_ATTEMPTS - failure_count)
            error = f"帳號或密碼錯誤。（剩餘嘗試次數：{remaining_attempts}）"
            return render_template(
                "login.html", error=error, auth_mode=mode, username=username
            ), 401
        finally:
            db_session.close()

    @app.route("/logout")
    def logout():
        if session.get("logged_in"):
            _audit_session_event(LoginHistory.STATUS_LOGOUT, "user_logout")
        session.clear()
        return redirect(url_for("login"))


print("[Security] Auth identity governance module loaded")
