"""
MFA enforcement policy.

Reads the relevant `settings` rows and decides whether a freshly-
authenticated user has to enrol TOTP before being allowed into a full
session. Centralised here so login + SSO callbacks call the exact same
logic.

Settings (KV in `settings` table):
- `mfa_enforced`              "true" | "false"        default: "false"
- `mfa_enforced_roles`        JSON list or csv        default: '["admin","editor"]'
- `mfa_enforce_sso_users`     "true" | "false"        default: "false"
                              (SAML/OIDC/LDAP users normally rely on
                              the IdP/AD-side MFA; flip on if you want
                              an additional TOTP layer.)

If `mfa_enforced=false` the function is a no-op — existing deploys
stay unchanged until an admin flips the toggle.
"""
from __future__ import annotations

import json
import logging
from typing import List

from sqlalchemy.orm import Session

from app.models.setting import Setting
from app.models.user import AuthProvider, User, UserRole

logger = logging.getLogger(__name__)

_KEY_ENFORCED = "mfa_enforced"
_KEY_ROLES = "mfa_enforced_roles"
_KEY_ENFORCE_SSO = "mfa_enforce_sso_users"

_DEFAULT_ROLES = [UserRole.ADMIN.value, UserRole.EDITOR.value]


def _bool(value: str | None) -> bool:
    return (value or "").strip().lower() in ("true", "1", "yes", "on")


def _read_setting(db: Session, key: str) -> str | None:
    row = db.query(Setting).filter(Setting.key == key).first()
    return row.value if row else None


def _parse_roles(value: str | None) -> List[str]:
    if not value:
        return list(_DEFAULT_ROLES)
    raw = value.strip()
    # Accept JSON array, csv, or single role string.
    if raw.startswith("["):
        try:
            arr = json.loads(raw)
            return [str(r).strip().lower() for r in arr if str(r).strip()]
        except json.JSONDecodeError:
            logger.warning("mfa_enforced_roles is not valid JSON: %r", raw)
            return list(_DEFAULT_ROLES)
    return [r.strip().lower() for r in raw.split(",") if r.strip()]


def is_enforced(db: Session) -> bool:
    """Global on/off toggle. Returns False unless setting explicitly true."""
    return _bool(_read_setting(db, _KEY_ENFORCED))


def enforced_roles(db: Session) -> List[str]:
    return _parse_roles(_read_setting(db, _KEY_ROLES))


def enforce_sso_users(db: Session) -> bool:
    return _bool(_read_setting(db, _KEY_ENFORCE_SSO))


def requires_setup(db: Session, user: User) -> bool:
    """
    True when this user MUST enrol TOTP right now before getting a JWT.

    Logic:
    - Master toggle off                                → False
    - User already has totp_enabled                    → False
    - User's role not in enforced roles list           → False
    - User authenticated via SSO/LDAP AND enforce_sso  → True
    - User authenticated via local                     → True

    Returns False on any DB hiccup — fail-open here is safer than
    locking everyone out if the settings table becomes unreadable.
    """
    try:
        if not is_enforced(db):
            return False
        if user.totp_enabled and user.totp_secret:
            return False
        role_val = user.role.value if hasattr(user.role, "value") else str(user.role)
        if role_val.lower() not in enforced_roles(db):
            return False
        if user.auth_provider != AuthProvider.LOCAL and not enforce_sso_users(db):
            return False
        return True
    except Exception as e:
        logger.warning("MFA policy check failed (fail-open): %s", e)
        return False
