from __future__ import annotations

from datetime import timedelta
from uuid import uuid4

from app.core.config import settings
from app.core.datetime_utils import ensure_utc, utc_now
from app.core.email import send_template_email
from app.core.jwt_denylist import bump_auth_epoch, revoke_access_token
from app.core.security import (
    create_access_token,
    generate_opaque_token,
    generate_refresh_token,
    hash_opaque_token,
    hash_password,
    hash_refresh_token,
    verify_password,
)
from app.modules.auth import repository as auth_repository
from app.modules.users import repository
from app.modules.users.models import User

def _token_expires_at():
    return utc_now() + timedelta(hours=settings.auth_token_ttl_hours)


def _send_verification_email(user: User, token: str) -> None:
    verify_url = f"{settings.frontend_url}/verify?token={token}"
    show_token = settings.email_delivery_mode == "memory" or settings.enable_test_routes
    token_line = f"TOKEN:{token}\n" if show_token else ""
    body = "Confirm your Compton account.\n\n" + f"Open: {verify_url}\n" + token_line
    send_template_email(
        to=user.email,
        template="verify_email",
        subject="Confirm your Compton account",
        body=body,
    )


def _send_password_reset_email(user: User, token: str) -> None:
    reset_url = f"{settings.frontend_url}/reset-password?token={token}"
    show_token = settings.email_delivery_mode == "memory" or settings.enable_test_routes
    token_line = f"TOKEN:{token}\n" if show_token else ""
    body = "Reset your Compton password.\n\n" + f"Open: {reset_url}\n" + token_line
    send_template_email(
        to=user.email,
        template="reset_password",
        subject="Reset your Compton password",
        body=body,
    )


def _issue_verification_token(user_id: str) -> str:
    token = generate_opaque_token()
    auth_repository.create_email_verification_token(
        user_id=user_id,
        token_hash=hash_opaque_token(token),
        expires_at=_token_expires_at(),
    )
    return token


def _issue_password_reset_token(user_id: str) -> str:
    token = generate_opaque_token()
    auth_repository.create_password_reset_token(
        user_id=user_id,
        token_hash=hash_opaque_token(token),
        expires_at=_token_expires_at(),
    )
    return token


def register(email: str, password: str) -> User:
    existing = repository.get_user_by_email(email)
    if existing:
        if existing.status == "pending":
            token = _issue_verification_token(existing.id)
            _send_verification_email(existing, token)
        return existing
    user = repository.create_user(email=email, password_hash=hash_password(password), status="pending")
    token = _issue_verification_token(user.id)
    _send_verification_email(user, token)
    return user


def verify_email_token(token: str) -> User:
    token_hash = hash_opaque_token(token)
    token_row = auth_repository.get_email_verification_token(token_hash)
    if not token_row or token_row.used_at is not None:
        raise ValueError("INVALID_TOKEN")
    if ensure_utc(token_row.expires_at) < utc_now():
        raise ValueError("TOKEN_EXPIRED")

    user = repository.get_user_by_id(token_row.user_id)
    if not user:
        raise ValueError("INVALID_TOKEN")

    user.status = "active"
    user.email_verified_at = utc_now()
    repository.update_user(user)
    auth_repository.mark_email_verification_token_used(token_hash)
    return user


def resend_verification(email: str) -> None:
    user = repository.get_user_by_email(email)
    if not user or user.status != "pending":
        return
    token = _issue_verification_token(user.id)
    _send_verification_email(user, token)


def forgot_password(email: str) -> None:
    user = repository.get_user_by_email(email)
    if not user or user.status == "blocked":
        return
    token = _issue_password_reset_token(user.id)
    _send_password_reset_email(user, token)


def reset_password(token: str, new_password: str) -> None:
    token_hash = hash_opaque_token(token)
    token_row = auth_repository.get_password_reset_token(token_hash)
    if not token_row or token_row.used_at is not None:
        raise ValueError("INVALID_TOKEN")
    if ensure_utc(token_row.expires_at) < utc_now():
        raise ValueError("TOKEN_EXPIRED")

    user = repository.get_user_by_id(token_row.user_id)
    if not user:
        raise ValueError("INVALID_TOKEN")

    user.password_hash = hash_password(new_password)
    repository.update_user(user)
    auth_repository.mark_password_reset_token_used(token_hash)
    bump_auth_epoch(user.id)
    revoke_user_refresh_family(user.id)


def _is_locked(user: User) -> bool:
    locked_until = ensure_utc(user.locked_until)
    if locked_until and locked_until > utc_now():
        return True
    if locked_until and locked_until <= utc_now():
        user.failed_login_attempts = 0
        user.locked_until = None
        repository.update_user(user)
    return False


def _record_failed_login(user: User) -> None:
    locked_until = ensure_utc(user.locked_until)
    if locked_until and locked_until <= utc_now():
        user.failed_login_attempts = 0
        user.locked_until = None
    user.failed_login_attempts += 1
    if user.failed_login_attempts >= settings.auth_lockout_attempts:
        user.locked_until = utc_now() + timedelta(minutes=settings.auth_lockout_minutes)
    repository.update_user(user)


def _reset_login_attempts(user: User) -> None:
    user.failed_login_attempts = 0
    user.locked_until = None
    repository.update_user(user)


def login(email: str, password: str) -> tuple[str, str, User]:
    user = repository.get_user_by_email(email)
    if user and _is_locked(user):
        raise PermissionError("ACCOUNT_TEMPORARILY_LOCKED")
    if not user or not verify_password(password, user.password_hash):
        if user:
            _record_failed_login(user)
        raise ValueError("INVALID_CREDENTIALS")
    if user.status == "pending":
        raise PermissionError("EMAIL_NOT_VERIFIED")
    if user.status == "blocked":
        raise PermissionError("ACCOUNT_BLOCKED")
    _reset_login_attempts(user)
    access_token = create_access_token(user.id, user.role, user.is_superuser)
    refresh_token = issue_refresh_token(user.id)
    return access_token, refresh_token, user


def issue_refresh_token(user_id: str, family_id: str | None = None) -> str:
    token = generate_refresh_token()
    token_hash = hash_refresh_token(token)
    family = family_id or str(uuid4())
    auth_repository.create_refresh_token(
        user_id=user_id,
        token_hash=token_hash,
        family_id=family,
        expires_at=utc_now() + timedelta(days=settings.jwt_refresh_ttl_days),
    )
    return token


def refresh(refresh_token: str) -> tuple[str, str, User]:
    token_hash = hash_refresh_token(refresh_token)
    token_row = auth_repository.get_refresh_token(token_hash)
    if not token_row:
        raise PermissionError("INVALID_REFRESH")

    user = repository.get_user_by_id(token_row.user_id)
    if not user:
        raise PermissionError("INVALID_REFRESH")
    if user.status == "pending":
        raise PermissionError("EMAIL_NOT_VERIFIED")
    if user.status == "blocked":
        raise PermissionError("ACCOUNT_BLOCKED")

    if token_row.revoked_at is not None:
        auth_repository.revoke_family_tokens(token_row.family_id)
        raise PermissionError("INVALID_REFRESH")
    if ensure_utc(token_row.expires_at) < utc_now():
        raise PermissionError("EXPIRED_REFRESH")

    auth_repository.revoke_refresh_token(token_hash)
    new_refresh = issue_refresh_token(user.id, family_id=token_row.family_id)
    access = create_access_token(user.id, user.role, user.is_superuser)
    return access, new_refresh, user


def revoke_refresh_token(refresh_token: str) -> None:
    token_hash = hash_refresh_token(refresh_token)
    token_row = auth_repository.revoke_refresh_token(token_hash)
    if token_row:
        auth_repository.revoke_family_tokens(token_row.family_id)


def logout(refresh_token: str | None, access_token: str | None) -> None:
    if access_token:
        revoke_access_token(access_token)
    if refresh_token:
        revoke_refresh_token(refresh_token)


def revoke_user_refresh_family(user_id: str) -> None:
    auth_repository.revoke_user_families(user_id)
