from __future__ import annotations

import logging
import secrets
import smtplib
from datetime import timedelta
from email.message import EmailMessage

from fastapi import HTTPException, status
from sqlalchemy import select, update, delete
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings
from app.core.security import (
    create_token,
    hash_password,
    hash_refresh_token,
    utcnow,
    verify_password,
)
from app.models.refresh_token import RefreshToken
from app.models.password_reset_token import PasswordResetToken
from app.models.user import User

logger = logging.getLogger(__name__)


def _access_expires() -> timedelta:
    return timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)


def _refresh_expires() -> timedelta:
    return timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)


def _reset_expires() -> timedelta:
    # tokens are valid for 10 minutes from creation
    return timedelta(minutes=10)


def _validate_password_complexity(password: str) -> None:
    # reduced to minimum length per updated requirements; other complexity rules
    # are intentionally dropped so users only need 8 characters.
    if len(password) < 8:
        raise HTTPException(status_code=400, detail="Password must be at least 8 characters")


def _send_reset_email(*, recipient: str, reset_link: str) -> None:
    host = settings.SMTP_HOST
    port = settings.SMTP_PORT
    smtp_user = settings.SMTP_USER
    smtp_pass = settings.SMTP_PASS
    sender = settings.SMTP_FROM

    if not host or not port or not sender:
        raise RuntimeError("SMTP configuration incomplete")

    msg = EmailMessage()
    msg["Subject"] = "Password Reset Link"
    msg["From"] = sender
    msg["To"] = recipient
    msg.set_content(
        """A request was received to reset your password.

Reset link:
{link}

This link will expire in 10 minutes. If you did not request this, you can ignore this email.
""".format(link=reset_link)
    )

    with smtplib.SMTP(host, port) as smtp:
        smtp.ehlo()
        smtp.starttls()
        smtp.ehlo()

        if smtp_user and smtp_pass:
            smtp.login(smtp_user.strip(), smtp_pass.replace(" ", "").strip())

        smtp.send_message(msg)


async def signup(db: AsyncSession, *, email: str, password: str, confirm_password: str) -> User:
    if password != confirm_password:
        raise HTTPException(status_code=400, detail="Passwords do not match")

    existing = await db.execute(select(User).where(User.email == email))
    if existing.scalar_one_or_none():
        raise HTTPException(status_code=409, detail="Email already registered")

    user = User(email=email, password_hash=hash_password(password))
    db.add(user)

    await db.commit()

    await db.refresh(user)
    return user


async def _cleanup_reset_tokens(db: AsyncSession) -> None:
    """Delete tokens that are expired or have already been used.

    This is invoked on each password-reset flow invocation to keep the table
    tidy and ensure tokens older than the configured expiry (10 minutes) do
    not linger indefinitely.  We commit immediately because callers usually
    perform their own commits afterwards.
    """
    now = utcnow()
    # only delete tokens that have passed their expiry; tokens that were used
    # remain until they naturally expire so that callers can return the
    # "already used" response during the valid window.
    result = await db.execute(
        delete(PasswordResetToken).where(PasswordResetToken.expires_at <= now)
    )
    await db.commit()
    try:
        count = result.rowcount
    except AttributeError:
        count = None
    logger.debug("Cleanup removed %s expired password-reset token(s)", count)


async def forgot_password(db: AsyncSession, *, email: str) -> tuple[bool, str | None]:
    # trim around the email early and run cleanup to purge old tokens
    email = email.strip()
    await _cleanup_reset_tokens(db)

    result = await db.execute(select(User).where(User.email == email))
    user = result.scalar_one_or_none()

    # We don't divulge whether the account exists or not to callers; the
    # endpoint always returns a generic success message.  For tests we
    # return a boolean (found) plus the raw token so that callers can
    # verify the reset link without having to read the logs.
    if not user or not user.is_active:
        logger.info("Password reset requested for email=%s (generic response)", email)
        return False, None

    raw_token = secrets.token_urlsafe(48)
    token_hash = hash_refresh_token(raw_token)

    prt = PasswordResetToken(
        user_id=user.id,
        token_hash=token_hash,
        expires_at=utcnow() + _reset_expires(),
        used_at=None,
    )
    db.add(prt)
    await db.commit()

    frontend = (settings.FRONTEND_URL or "").rstrip("/")
    reset_link = f"{frontend}/reset-password?token={raw_token}"

    try:
        _send_reset_email(recipient=email, reset_link=reset_link)
        logger.info("Password reset email sent. email=%s", email)
    except Exception as exc:
        logger.warning("Failed to send password reset email. email=%s error=%s", email, exc)
        logger.info("Password reset link (fallback log). email=%s link=%s", email, reset_link)
    return True, raw_token


async def reset_password(db: AsyncSession, *, token: str, new_password: str, confirm_password: str) -> None:
    await _cleanup_reset_tokens(db)

    if new_password != confirm_password:
        raise HTTPException(status_code=400, detail="Passwords do not match")

    _validate_password_complexity(new_password)

    token_hash = hash_refresh_token(token)
    result = await db.execute(select(PasswordResetToken).where(PasswordResetToken.token_hash == token_hash))
    stored = result.scalar_one_or_none()

    if not stored:
        raise HTTPException(status_code=400, detail="Invalid or Expired Link")

    now = utcnow()
    if stored.expires_at <= now:
        raise HTTPException(status_code=400, detail="Reset link has expired")
    if stored.used_at is not None:
        raise HTTPException(status_code=400, detail="This link has already been used")

    user_result = await db.execute(select(User).where(User.id == stored.user_id))
    user = user_result.scalar_one_or_none()
    if not user or not user.is_active:
        raise HTTPException(status_code=400, detail="Invalid or Expired Link")

    if verify_password(new_password, user.password_hash):
        raise HTTPException(status_code=400, detail="New password must not match the previous password")

    user.password_hash = hash_password(new_password)
    stored.used_at = now

    await db.execute(update(RefreshToken).where(RefreshToken.user_id == user.id).values(revoked=True))
    user.tokens_invalid_before = now

    await db.commit()
    return


async def login(db: AsyncSession, *, email: str, password: str) -> tuple[str, str]:
    result = await db.execute(select(User).where(User.email == email))
    user = result.scalar_one_or_none()
    if not user or not user.is_active:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")

    if not verify_password(password, user.password_hash):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")

    access_token = create_token(sub=str(user.id), email=user.email, token_type="access", expires_delta=_access_expires())
    refresh_token = create_token(sub=str(user.id), email=user.email, token_type="refresh", expires_delta=_refresh_expires())

    rt = RefreshToken(
        user_id=user.id,
        token_hash=hash_refresh_token(refresh_token),
        expires_at=utcnow() + _refresh_expires(),
        revoked=False,
    )
    db.add(rt)
    await db.commit()

    return access_token, refresh_token


async def refresh(db: AsyncSession, *, refresh_token: str) -> tuple[str, str]:
    from app.core.security import decode_token

    try:
        payload = decode_token(refresh_token)
    except ValueError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")

    if payload.get("type") != "refresh":
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")

    token_hash = hash_refresh_token(refresh_token)

    result = await db.execute(
        select(RefreshToken).where(RefreshToken.token_hash == token_hash)
    )
    stored = result.scalar_one_or_none()

    if not stored or stored.revoked or stored.expires_at <= utcnow():
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")

    user_result = await db.execute(select(User).where(User.id == stored.user_id))
    user = user_result.scalar_one_or_none()
    if not user or not user.is_active:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")

    stored.revoked = True

    new_access = create_token(sub=str(user.id), email=user.email, token_type="access", expires_delta=_access_expires())
    new_refresh = create_token(sub=str(user.id), email=user.email, token_type="refresh", expires_delta=_refresh_expires())

    new_stored = RefreshToken(
        user_id=user.id,
        token_hash=hash_refresh_token(new_refresh),
        expires_at=utcnow() + _refresh_expires(),
        revoked=False,
    )
    db.add(new_stored)

    await db.commit()

    return new_access, new_refresh


async def logout(db: AsyncSession, *, refresh_token: str) -> None:
    token_hash = hash_refresh_token(refresh_token)
    result = await db.execute(select(RefreshToken).where(RefreshToken.token_hash == token_hash))
    stored = result.scalar_one_or_none()
    if not stored:
        return

    await db.execute(
        update(RefreshToken)
        .where(RefreshToken.user_id == stored.user_id)
        .values(revoked=True)
    )
    await db.execute(
        update(User)
        .where(User.id == stored.user_id)
        .values(tokens_invalid_before=utcnow())
    )
    await db.commit()
