"""User self-service profile API.

Provides endpoints for the authenticated user to view and update their own
profile settings without requiring admin access.

Routes:
  GET    /api/profile          — read current user's profile
  PATCH  /api/profile          — update display name, language, theme
  POST   /api/profile/avatar   — upload a new profile picture (JPEG/PNG/GIF/WebP, max 2 MB)
  DELETE /api/profile/avatar   — remove custom avatar (reverts to Gravatar)
  POST   /api/profile/change-password — change password (local-auth users only)
"""

from __future__ import annotations

import base64
import logging
from hashlib import md5
from typing import Annotated

from fastapi import APIRouter, Depends, File, HTTPException, Request, Response, UploadFile, status
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session

from app.auth import require_login
from app.database import get_db
from app.models import LocalUser, UserProfile
from app.utils.i18n import SUPPORTED_LANGUAGE_CODES

logger = logging.getLogger(__name__)

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

DbSession = Annotated[Session, Depends(get_db)]

# Maximum avatar upload size: 2 MB
_MAX_AVATAR_BYTES = 2 * 1024 * 1024

# Allowed MIME types for avatar uploads
_ALLOWED_AVATAR_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}

# Valid theme values
_VALID_THEMES = {"light", "dark", "system"}


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _get_user_id(request: Request) -> str:
    """Return the stable user identifier from the session.

    Raises HTTP 401 if no user is logged in.
    """
    user = request.session.get("user")
    if not user or not isinstance(user, dict):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
    uid = user.get("sub") or user.get("preferred_username") or user.get("email") or user.get("id")
    if not uid:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Cannot determine user identity")
    return uid


def _gravatar_url(email: str | None) -> str:
    """Generate a Gravatar URL for *email*, falling back to identicon."""
    if not email:
        return "https://www.gravatar.com/avatar/?d=identicon"
    # MD5 used for Gravatar URL generation only — not for security
    h = md5(email.strip().lower().encode(), usedforsecurity=False).hexdigest()
    return f"https://www.gravatar.com/avatar/{h}?d=identicon"


def _get_or_create_profile(db: Session, user_id: str) -> UserProfile:
    """Return the UserProfile for *user_id*, creating a stub if one doesn't exist."""
    profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
    if profile is None:
        profile = UserProfile(user_id=user_id)
        db.add(profile)
        try:
            db.commit()
            db.refresh(profile)
        except Exception:
            db.rollback()
            raise
    return profile


# ---------------------------------------------------------------------------
# Pydantic schemas
# ---------------------------------------------------------------------------


class ProfileResponse(BaseModel):
    """Response body for GET /api/profile."""

    user_id: str
    display_name: str | None
    contact_email: str | None
    preferred_language: str | None
    preferred_theme: str | None
    default_document_language: str | None
    """ISO 639-1 code for the user's preferred document translation target language."""
    avatar_url: str
    """Gravatar URL or ``data:`` URI for a custom uploaded avatar."""
    is_local_user: bool
    """True when the account was created via local email/password sign-up."""


class ProfileUpdateRequest(BaseModel):
    """Request body for PATCH /api/profile."""

    display_name: str | None = Field(default=None, max_length=255, description="Human-readable display name")
    contact_email: str | None = Field(default=None, max_length=255, description="Contact / notification e-mail")
    preferred_language: str | None = Field(default=None, description="ISO 639-1 language code, e.g. 'en', 'de'")
    preferred_theme: str | None = Field(default=None, description="Colour scheme: 'light', 'dark', or 'system'")
    default_document_language: str | None = Field(
        default=None,
        description="ISO 639-1 code for the default document translation target language, e.g. 'en', 'de'",
    )


class ChangePasswordRequest(BaseModel):
    """Request body for POST /api/profile/change-password."""

    current_password: str = Field(..., min_length=1, max_length=128)
    new_password: str = Field(..., min_length=8, max_length=128)
    new_password_confirm: str = Field(..., min_length=8, max_length=128)


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@router.get("", response_model=ProfileResponse)
@require_login
async def get_profile(request: Request, db: DbSession) -> ProfileResponse:
    """Return the current user's profile settings."""
    user_id = _get_user_id(request)
    profile = _get_or_create_profile(db, user_id)

    session_user = request.session.get("user", {})
    email = session_user.get("email") if isinstance(session_user, dict) else None

    # Determine avatar: prefer stored data, fall back to Gravatar
    avatar_url = profile.avatar_data if profile.avatar_data else _gravatar_url(email)  # type: ignore[attr-defined]

    # Check whether this is a local (email/password) account
    is_local = db.query(LocalUser).filter(LocalUser.username == user_id).first() is not None

    return ProfileResponse(
        user_id=user_id,
        display_name=profile.display_name,  # type: ignore[arg-type]
        contact_email=profile.contact_email,  # type: ignore[arg-type]
        preferred_language=profile.preferred_language,  # type: ignore[arg-type]
        preferred_theme=profile.preferred_theme,  # type: ignore[arg-type]
        default_document_language=profile.default_document_language,  # type: ignore[arg-type]
        avatar_url=avatar_url,
        is_local_user=is_local,
    )


@router.patch("", response_model=ProfileResponse)
@require_login
async def update_profile(
    body: ProfileUpdateRequest, request: Request, response: Response, db: DbSession
) -> ProfileResponse:
    """Update the current user's editable profile settings."""
    user_id = _get_user_id(request)
    profile = _get_or_create_profile(db, user_id)

    # Validate language code
    if body.preferred_language is not None:
        lang = body.preferred_language.lower().strip()
        if lang and lang not in SUPPORTED_LANGUAGE_CODES:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=f"Unsupported language code: {lang}",
            )
        profile.preferred_language = lang or None  # type: ignore[assignment]

        # Keep session and cookie in sync so detect_language() picks up
        # the new preference immediately (without a DB round-trip).
        if hasattr(request, "session"):
            if lang:
                request.session["preferred_language"] = lang
            else:
                request.session.pop("preferred_language", None)
        if lang:
            response.set_cookie(
                key="docuelevate_lang",
                value=lang,
                max_age=30 * 24 * 60 * 60,
                httponly=False,
                samesite="lax",
            )
        else:
            response.delete_cookie(key="docuelevate_lang")

    # Validate theme
    if body.preferred_theme is not None:
        theme = body.preferred_theme.lower().strip()
        if theme and theme not in _VALID_THEMES:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=f"Invalid theme: {theme}. Must be one of: {', '.join(sorted(_VALID_THEMES))}",
            )
        profile.preferred_theme = theme or None  # type: ignore[assignment]

    # Validate default document language
    if body.default_document_language is not None:
        doc_lang = body.default_document_language.lower().strip()
        if doc_lang and doc_lang not in SUPPORTED_LANGUAGE_CODES:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=f"Unsupported language code: {doc_lang}",
            )
        profile.default_document_language = doc_lang or None  # type: ignore[assignment]

    if body.display_name is not None:
        profile.display_name = body.display_name.strip() or None  # type: ignore[assignment]

    if body.contact_email is not None:
        profile.contact_email = body.contact_email.strip() or None  # type: ignore[assignment]

    try:
        db.commit()
        db.refresh(profile)
    except Exception:
        db.rollback()
        raise

    session_user = request.session.get("user", {})
    email = session_user.get("email") if isinstance(session_user, dict) else None
    avatar_url = profile.avatar_data if profile.avatar_data else _gravatar_url(email)  # type: ignore[attr-defined]
    is_local = db.query(LocalUser).filter(LocalUser.username == user_id).first() is not None

    return ProfileResponse(
        user_id=user_id,
        display_name=profile.display_name,  # type: ignore[arg-type]
        contact_email=profile.contact_email,  # type: ignore[arg-type]
        preferred_language=profile.preferred_language,  # type: ignore[arg-type]
        preferred_theme=profile.preferred_theme,  # type: ignore[arg-type]
        default_document_language=profile.default_document_language,  # type: ignore[arg-type]
        avatar_url=avatar_url,
        is_local_user=is_local,
    )


@router.post("/avatar", status_code=status.HTTP_200_OK)
@require_login
async def upload_avatar(
    request: Request,
    db: DbSession,
    file: UploadFile = File(..., description="Profile picture (JPEG, PNG, GIF or WebP; max 2 MB)"),
) -> dict:
    """Upload a new profile picture.

    The image is stored as a base64-encoded data URL in ``UserProfile.avatar_data``.
    Accepts JPEG, PNG, GIF, or WebP files up to 2 MB.
    """
    user_id = _get_user_id(request)

    content_type = (file.content_type or "").lower()
    if content_type not in _ALLOWED_AVATAR_TYPES:
        raise HTTPException(
            status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
            detail=f"Unsupported image type '{content_type}'. Allowed: JPEG, PNG, GIF, WebP.",
        )

    # Check declared size first (available when the client sends a Content-Length header)
    if file.size is not None and file.size > _MAX_AVATAR_BYTES:
        raise HTTPException(
            status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
            detail="Avatar image must be 2 MB or smaller.",
        )

    # Read up to one byte past the limit so we can detect oversized uploads
    raw = await file.read(_MAX_AVATAR_BYTES + 1)
    if len(raw) > _MAX_AVATAR_BYTES:
        raise HTTPException(
            status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
            detail="Avatar image must be 2 MB or smaller.",
        )

    b64 = base64.b64encode(raw).decode("ascii")
    data_url = f"data:{content_type};base64,{b64}"

    profile = _get_or_create_profile(db, user_id)
    profile.avatar_data = data_url  # type: ignore[assignment]
    try:
        db.commit()
    except Exception:
        db.rollback()
        raise

    return {"avatar_url": data_url}


@router.delete("/avatar", status_code=status.HTTP_200_OK)
@require_login
async def delete_avatar(request: Request, db: DbSession) -> dict:
    """Remove the custom avatar and revert to the Gravatar fallback."""
    user_id = _get_user_id(request)
    profile = _get_or_create_profile(db, user_id)
    profile.avatar_data = None  # type: ignore[assignment]
    try:
        db.commit()
    except Exception:
        db.rollback()
        raise

    session_user = request.session.get("user", {})
    email = session_user.get("email") if isinstance(session_user, dict) else None
    return {"avatar_url": _gravatar_url(email)}


@router.post("/change-password", status_code=status.HTTP_200_OK)
@require_login
async def change_password(body: ChangePasswordRequest, request: Request, db: DbSession) -> dict:
    """Change the password for local (email/password) accounts.

    Raises 403 if the account is not a local account or the current password is wrong.
    Raises 422 if the new passwords do not match.
    """
    from app.utils.local_auth import hash_password, verify_password

    user_id = _get_user_id(request)

    local_user = db.query(LocalUser).filter(LocalUser.username == user_id).first()
    if local_user is None:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Password change is only available for local accounts.",
        )

    if not verify_password(body.current_password, local_user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Current password is incorrect.",
        )

    if body.new_password != body.new_password_confirm:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="New passwords do not match.",
        )

    local_user.hashed_password = hash_password(body.new_password)
    try:
        db.commit()
    except Exception:
        db.rollback()
        raise

    logger.info("Password changed for local user: %s", user_id)
    return {"detail": "Password changed successfully."}
