import datetime as dt
import hashlib
import secrets

from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session

from flyway.auth.passwords import hash_password, verify_password
from flyway.config import settings
from flyway.domain.actor import Actor
from flyway.domain.errors import BusinessError
from flyway.models import ADMIN_ROLE, AppSession, AppUser, AppUserRole

MAX_FAILURES = 5
FAILURE_WINDOW = dt.timedelta(minutes=10)
INVALID = "Invalid user name or password."


def _digest(token: str) -> str:
    return hashlib.sha256(token.encode()).hexdigest()


def find_user(session: Session, username: str) -> AppUser | None:
    return session.scalar(select(AppUser).where(func.lower(AppUser.username) == (username or "").strip().lower()))


def create_user(session: Session, username: str, password: str, *, first_name="", last_name="", email="",
                admin=False) -> AppUser:
    username = (username or "").strip()
    if not username:
        raise BusinessError('"User name" is required.', "username")
    if find_user(session, username):
        raise BusinessError(f'The user name "{username}" already exists.', "username")
    user = AppUser(username=username, password_hash=hash_password(password),
                   first_name=first_name, last_name=last_name, email=email)
    if admin:
        user.roles.append(AppUserRole(role=ADMIN_ROLE))
    session.add(user)
    session.flush()
    return user


def authenticate(session: Session, username: str, password: str, now: dt.datetime) -> AppUser:
    user = find_user(session, username)
    if user is None or not user.is_active or user.is_locked_out:
        raise BusinessError(INVALID)
    if verify_password(user.password_hash, password):
        user.failed_attempts, user.failed_window_start, user.last_login_at = 0, None, now
        session.flush()
        return user
    if user.failed_window_start is None or now - user.failed_window_start > FAILURE_WINDOW:
        user.failed_attempts, user.failed_window_start = 1, now
    else:
        user.failed_attempts += 1
    if user.failed_attempts >= MAX_FAILURES:
        user.is_locked_out = True
    session.flush()
    raise BusinessError(INVALID)


def create_session(session: Session, user: AppUser, now: dt.datetime, persistent: bool = False) -> tuple[str, str]:
    token, csrf = secrets.token_urlsafe(32), secrets.token_urlsafe(32)
    session.add(AppSession(token_hash=_digest(token), user_id=user.id, csrf_token=csrf, created_at=now,
                           expires_at=now + dt.timedelta(hours=settings.session_hours), persistent=persistent))
    session.flush()
    return token, csrf


def load_session(session: Session, token: str | None, now: dt.datetime) -> AppSession | None:
    if not token:
        return None
    s = session.get(AppSession, _digest(token))
    if s is None or s.expires_at <= now or not s.user.is_active or s.user.is_locked_out:
        return None
    return s


def renew_session(session: Session, s: AppSession, now: dt.datetime) -> bool:
    """滑动过期（旧 Forms 认证 slidingExpiration）：剩余有效期不足一半时从现在起重新计时。返回是否续期。"""
    lifetime = dt.timedelta(hours=settings.session_hours)
    if s.expires_at - now >= lifetime / 2:
        return False
    s.expires_at = now + lifetime
    session.flush()
    return True


def delete_session(session: Session, token: str | None) -> None:
    if token:
        session.execute(delete(AppSession).where(AppSession.token_hash == _digest(token)))


def change_password(session: Session, user: AppUser, current: str, new: str) -> None:
    if not verify_password(user.password_hash, current):
        raise BusinessError("The current password is incorrect.", "current_password")
    user.password_hash = hash_password(new)
    session.flush()


def actor_for(user: AppUser) -> Actor:
    return Actor(user_id=user.id, username=user.username, full_name=user.full_name, is_admin=user.is_admin)

def update_profile(session: Session, user: AppUser, *, first_name: str, last_name: str, email: str,
                   mobile: str) -> AppUser:
    user.first_name, user.last_name = first_name.strip(), last_name.strip()
    user.email, user.mobile = email.strip(), mobile.strip()
    session.flush()
    return user


def set_admin(user: AppUser, admin: bool) -> None:
    if admin and not user.is_admin:
        user.roles.append(AppUserRole(role=ADMIN_ROLE))
    if not admin:
        user.roles = [r for r in user.roles if r.role != ADMIN_ROLE]


def admin_set_password(session: Session, user: AppUser, password: str) -> None:
    """管理员重置员工密码（文档 8.2 旧系统缺失、12.2 新增）。"""
    user.password_hash = hash_password(password)
    user.is_locked_out, user.failed_attempts, user.failed_window_start = False, 0, None
    session.execute(delete(AppSession).where(AppSession.user_id == user.id))
    session.flush()


def set_locked(session: Session, ids, locked: bool) -> None:
    """锁定 = 停用账号并踢下线（旧 LockOut 置 IsApproved=false）；解锁同时清除失败锁定。"""
    for user in session.scalars(select(AppUser).where(AppUser.id.in_(list(ids)))).all():
        user.is_active = not locked
        if not locked:
            user.is_locked_out, user.failed_attempts, user.failed_window_start = False, 0, None
        else:
            session.execute(delete(AppSession).where(AppSession.user_id == user.id))
    session.flush()


def delete_users(session: Session, actor: Actor, ids) -> None:
    ids = list(ids)
    if actor.user_id in ids:
        raise BusinessError("You can't delete your own account.")
    for user in session.scalars(select(AppUser).where(AppUser.id.in_(ids))).all():
        session.delete(user)
    session.flush()
