import json
from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, joinedload

from .. import models, schemas
from ..db import get_db
from ..cache import CACHE_TTL_SECONDS, get_cache, safe_cache_delete, safe_cache_set
from ..security import require_admin
from ..send_limits import (
    clear_account_send_limits,
    effective_limits_payload,
    publish_account_send_limits,
    publish_global_send_limits,
    sync_all_account_send_limits,
)
from ..utils import hash_password, remove_mailbox

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


def get_domain_by_name(db: Session, name: str, *, for_update: bool = False) -> models.Domain:
    query = db.query(models.Domain).filter(models.Domain.name == name)
    if for_update:
        query = query.with_for_update()
    domain = query.first()
    if not domain:
        raise HTTPException(status_code=404, detail="Domain not found")
    if not domain.is_active:
        raise HTTPException(status_code=400, detail="Domain is inactive")
    return domain


def cache_account(cache, email: str, account: models.Account) -> None:
    payload = {
        "id": account.id,
        "email": email,
        "domain_id": account.domain_id,
        "quota_mb": account.quota_mb,
        "is_active": account.is_active,
    }
    payload.update(effective_limits_payload(account))
    safe_cache_set(cache, f"account:{email}", json.dumps(payload), ex=CACHE_TTL_SECONDS)
    publish_account_send_limits(email, account)


def serialize_account(account: models.Account) -> dict:
    domain = account.domain
    email = f"{account.local_part}@{domain.name}" if domain else account.local_part
    payload = {
        "id": account.id,
        "email": email,
        "domain_id": account.domain_id,
        "quota_mb": account.quota_mb,
        "send_rate_burst": int(getattr(account, "send_rate_burst", 0) or 0),
        "send_rate_per_minute": int(getattr(account, "send_rate_per_minute", 0) or 0),
        "send_rate_per_hour": int(getattr(account, "send_rate_per_hour", 0) or 0),
        "send_rate_per_day": int(getattr(account, "send_rate_per_day", 0) or 0),
        "send_rate_per_week": int(getattr(account, "send_rate_per_week", 0) or 0),
        "send_rate_per_month": int(getattr(account, "send_rate_per_month", 0) or 0),
        "require_app_password": bool(getattr(account, "require_app_password", False)),
        "mfa_hint": bool(getattr(account, "mfa_hint", False)),
        "is_active": account.is_active,
        "created_at": account.created_at,
    }
    payload.update(effective_limits_payload(account))
    return payload


def _apply_send_limit_fields(account: models.Account, payload) -> None:
    for field in (
        "send_rate_burst",
        "send_rate_per_minute",
        "send_rate_per_hour",
        "send_rate_per_day",
        "send_rate_per_week",
        "send_rate_per_month",
    ):
        value = getattr(payload, field, None)
        if value is not None:
            setattr(account, field, value)


@router.get("/", response_model=List[schemas.AccountOut])
def list_accounts(domain: Optional[str] = Query(default=None), db: Session = Depends(get_db), _: str = Depends(require_admin)):
    query = db.query(models.Account).options(joinedload(models.Account.domain))
    if domain:
        dom = get_domain_by_name(db, domain)
        query = query.filter(models.Account.domain_id == dom.id)
    return [serialize_account(account) for account in query.all()]


@router.post("/send-limits/sync", response_model=dict)
def sync_send_limits(db: Session = Depends(get_db), _: str = Depends(require_admin)):
    """Rebuild Redis + map profiles for every mailbox from the database and global defaults."""
    publish_global_send_limits()
    accounts = db.query(models.Account).options(joinedload(models.Account.domain)).all()
    result = sync_all_account_send_limits(accounts)
    return {"status": "ok", **result}


@router.get("/{account_id}", response_model=schemas.AccountOut)
def get_account(account_id: int, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    account = db.query(models.Account).options(joinedload(models.Account.domain)).filter(models.Account.id == account_id).first()
    if not account:
        raise HTTPException(status_code=404, detail="Account not found")
    return serialize_account(account)


@router.post("/", response_model=schemas.AccountOut)
def create_account(payload: schemas.AccountCreate, db: Session = Depends(get_db), cache=Depends(get_cache), _: str = Depends(require_admin)):
    domain = get_domain_by_name(db, payload.domain, for_update=True)
    email = f"{payload.local_part}@{domain.name}"
    active_accounts = db.query(models.Account).filter(
        models.Account.domain_id == domain.id,
        models.Account.is_active.is_(True),
    ).count()
    if payload.is_active and domain.max_users and active_accounts >= domain.max_users:
        raise HTTPException(status_code=400, detail="Domain account limit reached")
    existing = db.query(models.Account).filter(models.Account.domain_id == domain.id, models.Account.local_part == payload.local_part).first()
    if existing:
        raise HTTPException(status_code=400, detail="Account already exists")
    account = models.Account(
        domain_id=domain.id,
        local_part=payload.local_part,
        username=email,
        password_hash=hash_password(payload.password),
        quota_mb=payload.quota_mb,
        send_rate_burst=payload.send_rate_burst,
        send_rate_per_minute=payload.send_rate_per_minute,
        send_rate_per_hour=payload.send_rate_per_hour,
        send_rate_per_day=payload.send_rate_per_day,
        send_rate_per_week=payload.send_rate_per_week,
        send_rate_per_month=payload.send_rate_per_month,
        is_active=payload.is_active,
    )
    db.add(account)
    try:
        db.commit()
    except IntegrityError as exc:
        db.rollback()
        raise HTTPException(status_code=409, detail="Account already exists") from exc
    db.refresh(account)
    account.domain = domain
    cache_account(cache, email, account)
    return serialize_account(account)


@router.patch("/{account_id}", response_model=schemas.AccountOut)
def update_account(account_id: int, payload: schemas.AccountUpdate, db: Session = Depends(get_db), cache=Depends(get_cache), _: str = Depends(require_admin)):
    account = (
        db.query(models.Account)
        .options(joinedload(models.Account.domain))
        .filter(models.Account.id == account_id)
        .with_for_update()
        .first()
    )
    if not account:
        raise HTTPException(status_code=404, detail="Account not found")
    domain = account.domain
    if payload.is_active is True and domain and domain.max_users:
        db.query(models.Domain).filter(models.Domain.id == domain.id).with_for_update().first()
        active_accounts = db.query(models.Account).filter(
            models.Account.domain_id == domain.id,
            models.Account.is_active.is_(True),
            models.Account.id != account.id,
        ).count()
        if active_accounts >= domain.max_users:
            raise HTTPException(status_code=400, detail="Domain account limit reached")
    if payload.password:
        account.password_hash = hash_password(payload.password)
    if payload.quota_mb is not None:
        account.quota_mb = payload.quota_mb
    if payload.is_active is not None:
        account.is_active = payload.is_active
    if payload.require_app_password is not None:
        account.require_app_password = payload.require_app_password
    if payload.mfa_hint is not None:
        account.mfa_hint = payload.mfa_hint
    _apply_send_limit_fields(account, payload)
    db.commit()
    db.refresh(account)
    email = f"{account.local_part}@{domain.name}" if domain else account.local_part
    cache_account(cache, email, account)
    return serialize_account(account)


@router.get("/{account_id}/app-passwords", response_model=List[schemas.AppPasswordOut])
def list_app_passwords(account_id: int, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    account = db.query(models.Account).filter(models.Account.id == account_id).first()
    if not account:
        raise HTTPException(status_code=404, detail="Account not found")
    rows = (
        db.query(models.MailboxAppPassword)
        .filter(models.MailboxAppPassword.account_id == account_id)
        .order_by(models.MailboxAppPassword.id.desc())
        .all()
    )
    return [
        schemas.AppPasswordOut(
            id=row.id,
            label=row.label,
            created_at=row.created_at,
            last_used_at=row.last_used_at,
            revoked_at=row.revoked_at,
        )
        for row in rows
    ]


@router.post("/{account_id}/app-passwords", response_model=schemas.AppPasswordCreatedOut)
def create_app_password(
    account_id: int,
    payload: schemas.AppPasswordCreate,
    db: Session = Depends(get_db),
    _: str = Depends(require_admin),
):
    from datetime import UTC, datetime

    from ..mfa import generate_app_password_plaintext, hash_app_password

    account = db.query(models.Account).filter(models.Account.id == account_id).first()
    if not account:
        raise HTTPException(status_code=404, detail="Account not found")
    plaintext = generate_app_password_plaintext()
    row = models.MailboxAppPassword(
        account_id=account.id,
        label=(payload.label or "client")[:128],
        password_hash=hash_app_password(plaintext),
        created_at=datetime.now(UTC).replace(tzinfo=None),
    )
    db.add(row)
    db.commit()
    db.refresh(row)
    return schemas.AppPasswordCreatedOut(
        id=row.id,
        label=row.label,
        created_at=row.created_at,
        last_used_at=row.last_used_at,
        revoked_at=row.revoked_at,
        password=plaintext,
    )


@router.delete("/{account_id}/app-passwords/{password_id}", response_model=dict)
def revoke_app_password(
    account_id: int,
    password_id: int,
    db: Session = Depends(get_db),
    _: str = Depends(require_admin),
):
    from datetime import UTC, datetime

    row = (
        db.query(models.MailboxAppPassword)
        .filter(
            models.MailboxAppPassword.id == password_id,
            models.MailboxAppPassword.account_id == account_id,
        )
        .first()
    )
    if not row:
        raise HTTPException(status_code=404, detail="App password not found")
    if not row.revoked_at:
        row.revoked_at = datetime.now(UTC).replace(tzinfo=None)
        db.commit()
    return {"revoked": True, "id": password_id}


@router.delete("/{account_id}", response_model=dict)
def delete_account(account_id: int, db: Session = Depends(get_db), cache=Depends(get_cache), _: str = Depends(require_admin)):
    account = db.query(models.Account).filter(models.Account.id == account_id).first()
    if not account:
        raise HTTPException(status_code=404, detail="Account not found")
    domain = account.domain
    local_part = account.local_part
    domain_name = domain.name if domain else None
    email = f"{local_part}@{domain_name}" if domain_name else local_part
    db.delete(account)
    db.commit()
    safe_cache_delete(cache, f"account:{email}")
    clear_account_send_limits(email)
    if domain_name:
        remove_mailbox(domain_name, local_part)
    return {"deleted": True, "email": email}
