from __future__ import annotations

from datetime import timedelta, timezone
import hashlib
import secrets

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr
from sqlalchemy.orm import Session
from sqlalchemy import select

from app.core.db.deps import get_common_db
from app.core.settings import get_settings
from app.core.security.passwords import verify_password
from app.core.security.jwt_tokens import encode_access_token, decode_token, utcnow
from app.modules.core.audit.service import write_audit_log
from app.modules.core.iam.invite_service import accept_invite, issue_password_reset_token, reset_password_with_token
from app.modules.email_integration.services import send_password_reset_link_email, send_password_changed_email
from app.modules.core.iam.models import User
from app.modules.core.iam.tokens_models import RefreshToken
from app.modules.core.rbac.models import Role, UserRole

router = APIRouter(prefix="/auth", tags=["auth"])

class TokenRequest(BaseModel):
    email: EmailStr
    password: str

class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"
    expires_in: int
    refresh_token: str

class ForgotPasswordRequest(BaseModel):
    email: EmailStr

class ResetPasswordRequest(BaseModel):
    token: str
    new_password: str

class AcceptInviteRequest(BaseModel):
    token: str
    password: str

def _hash_refresh(rt: str) -> str:
    return hashlib.sha256(rt.encode("utf-8")).hexdigest()

def _roles(db: Session, user_id: int) -> list[str]:
    q = select(Role.name).join(UserRole, UserRole.role_id == Role.id).where(UserRole.user_id == user_id)
    return [r for (r,) in db.execute(q).all()]

def _issue_tokens(db: Session, user: User) -> TokenResponse:
    s = get_settings()
    roles = _roles(db, user.id)
    payload = {
        "sub": str(user.id),
        "email": user.email,
        "tenant_id": user.tenant_id,
        "branch_id": user.branch_id,
        "roles": roles,
    }
    access = encode_access_token(payload, expires_minutes=s.JWT_ACCESS_MINUTES)

    refresh_plain = secrets.token_urlsafe(48)
    now = utcnow()
    exp = now + timedelta(days=s.JWT_REFRESH_DAYS)

    rt = RefreshToken(
        user_id=user.id,
        token_hash=_hash_refresh(refresh_plain),
        created_at_utc=now,
        expires_at_utc=exp,
        revoked=False,
        rotated_from_id=None,
    )
    db.add(rt)
    db.commit()

    return TokenResponse(access_token=access, expires_in=s.JWT_ACCESS_MINUTES * 60, refresh_token=refresh_plain)

@router.post("/token", response_model=TokenResponse)
def token(req: TokenRequest, db: Session = Depends(get_common_db)):
    user = db.execute(select(User).where(User.email == req.email.lower().strip())).scalar_one_or_none()
    if (not user or not user.is_active or not getattr(user, "allow_login", True) or getattr(user, "is_locked", False) or getattr(user, "deleted_at", None) is not None or not verify_password(req.password, user.password_hash)):
        write_audit_log(db, action="auth.token.failed", entity_type="api_session", actor=user, actor_email=req.email.lower().strip(), status="error", target_tenant_id=(user.tenant_id if user else None), target_branch_id=(user.branch_id if user else None), details={"reason": "invalid credentials"})
        raise HTTPException(status_code=401, detail="Invalid credentials")
    if getattr(user, "must_change_password", False):
        raise HTTPException(status_code=403, detail="Password setup/change required before API login")
    token_response = _issue_tokens(db, user)
    write_audit_log(db, action="auth.token.success", entity_type="api_session", actor=user, entity_name=user.email, target_tenant_id=user.tenant_id, target_branch_id=user.branch_id)
    return token_response

@router.post('/forgot-password')
def forgot_password(req: ForgotPasswordRequest, db: Session = Depends(get_common_db)):
    user = db.execute(select(User).where(User.email == req.email.lower().strip())).scalar_one_or_none()
    if user and user.is_active and getattr(user, "deleted_at", None) is None:
        reset_token = issue_password_reset_token(db, user)
        try:
            send_password_reset_link_email(db, user=user, reset_token=reset_token)
        except Exception as exc:
            write_audit_log(db, action="auth.password_reset.email_failed", entity_type="user", actor=user, entity_id=user.id, entity_name=user.email, status="error", target_tenant_id=user.tenant_id, target_branch_id=user.branch_id, details={"error": str(exc)})
        write_audit_log(db, action="auth.password_reset.requested", entity_type="user", actor=user, entity_id=user.id, entity_name=user.email, target_tenant_id=user.tenant_id, target_branch_id=user.branch_id)
        db.commit()
    return {"status": "ok"}

@router.post('/reset-password')
def reset_password(req: ResetPasswordRequest, db: Session = Depends(get_common_db)):
    try:
        user = reset_password_with_token(db, req.token, req.new_password)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc))
    if not user:
        raise HTTPException(status_code=400, detail="Invalid or expired reset token")
    try:
        send_password_changed_email(db, user=user)
    except Exception as exc:
        write_audit_log(db, action="auth.password_changed.email_failed", entity_type="user", actor=user, entity_id=user.id, entity_name=user.email, status="error", target_tenant_id=user.tenant_id, target_branch_id=user.branch_id, details={"error": str(exc)})
    write_audit_log(db, action="auth.password_reset.completed", entity_type="user", actor=user, entity_id=user.id, entity_name=user.email, target_tenant_id=user.tenant_id, target_branch_id=user.branch_id)
    db.commit()
    return {"status": "ok"}

@router.post('/invite/accept')
def invite_accept(req: AcceptInviteRequest, db: Session = Depends(get_common_db)):
    try:
        user = accept_invite(db, req.token, req.password)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc))
    if not user:
        raise HTTPException(status_code=400, detail="Invalid or expired invite token")
    write_audit_log(db, action="auth.invite.accepted", entity_type="user", actor=user, entity_id=user.id, entity_name=user.email, target_tenant_id=user.tenant_id, target_branch_id=user.branch_id)
    return {"status": "ok"}

class RefreshRequest(BaseModel):
    refresh_token: str

@router.post("/refresh", response_model=TokenResponse)
def refresh(req: RefreshRequest, db: Session = Depends(get_common_db)):
    h = _hash_refresh(req.refresh_token)
    rt = db.execute(select(RefreshToken).where(RefreshToken.token_hash == h)).scalar_one_or_none()
    if not rt or rt.revoked:
        raise HTTPException(status_code=401, detail="Invalid refresh token")
    now = utcnow()
    if rt.expires_at_utc.replace(tzinfo=timezone.utc) < now:
        raise HTTPException(status_code=401, detail="Refresh token expired")

    user = db.execute(select(User).where(User.id == rt.user_id)).scalar_one_or_none()
    if not user or not user.is_active or not getattr(user, "allow_login", True) or getattr(user, "is_locked", False) or getattr(user, "deleted_at", None) is not None:
        raise HTTPException(status_code=401, detail="User inactive")

    rt.revoked = True
    db.commit()
    token_response = _issue_tokens(db, user)
    write_audit_log(db, action="auth.token.refresh", entity_type="api_session", actor=user, entity_name=user.email, target_tenant_id=user.tenant_id, target_branch_id=user.branch_id)
    return token_response

class LogoutRequest(BaseModel):
    refresh_token: str

@router.post("/logout")
def logout(req: LogoutRequest, db: Session = Depends(get_common_db)):
    h = _hash_refresh(req.refresh_token)
    rt = db.execute(select(RefreshToken).where(RefreshToken.token_hash == h)).scalar_one_or_none()
    if rt:
        rt.revoked = True
        db.commit()
        user = db.execute(select(User).where(User.id == rt.user_id)).scalar_one_or_none()
        write_audit_log(db, action="auth.token.logout", entity_type="api_session", actor=user, entity_name=(user.email if user else None), target_tenant_id=(user.tenant_id if user else None), target_branch_id=(user.branch_id if user else None))
    return {"status": "ok"}

@router.get("/me")
def me(token: str):
    return {"token": decode_token(token)}
