"""Admin-only onboarding: review access requests (Invites), approve/deny, manual add.
Also the demo-account admin surface: the demo email whitelist + a manual state reset."""
from datetime import datetime, timezone

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session

from app import audit
from app import email as email_mod
from app.audit import AuditAction
from app.auth import (
    admin_user,
    count_admins,
    get_or_create_demo_user,
    operator_contact,
    purge_user,
)
from app.db import get_db
from app.models import (
    DemoWhitelist,
    Invite,
    Playlist,
    PlaylistItem,
    Subscription,
    User,
    Video,
    VideoState,
)
from app.utils import valid_email

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


def _serialize(inv: Invite) -> dict:
    return {
        "id": inv.id,
        "email": inv.email,
        "status": inv.status,
        "note": inv.note,
        "requested_at": inv.requested_at.isoformat() if inv.requested_at else None,
        "decided_at": inv.decided_at.isoformat() if inv.decided_at else None,
        "decided_by": inv.decided_by,
    }


@router.get("/invites")
def list_invites(
    status: str | None = None,
    _: User = Depends(admin_user),
    db: Session = Depends(get_db),
) -> list[dict]:
    stmt = select(Invite)
    if status:
        stmt = stmt.where(Invite.status == status)
    # Pending first, then most-recently requested.
    rows = db.execute(stmt.order_by(Invite.requested_at.desc())).scalars().all()
    order = {"pending": 0, "approved": 1, "denied": 2}
    rows.sort(key=lambda i: order.get(i.status, 9))
    return [_serialize(i) for i in rows]


def _activate_user_by_email(db: Session, email: str) -> None:
    """Approving access activates a matching (e.g. pending password-registration) account so
    it can sign in. No-op if no such user exists yet (a Google user is created at first login)."""
    user = db.execute(select(User).where(User.email == email.lower())).scalar_one_or_none()
    if user is not None and not user.is_active:
        user.is_active = True
        db.commit()


def _decide(db: Session, invite_id: int, admin: User, approved: bool) -> Invite:
    inv = db.get(Invite, invite_id)
    if inv is None:
        raise HTTPException(status_code=404, detail="No such invite")
    inv.status = "approved" if approved else "denied"
    inv.decided_at = datetime.now(timezone.utc)
    inv.decided_by = admin.email
    audit.record(
        db,
        admin.id,
        AuditAction.INVITE_APPROVE if approved else AuditAction.INVITE_DENY,
        target_type="invite",
        target_id=inv.id,
        summary=inv.email,
    )
    db.commit()
    return inv


@router.post("/invites/{invite_id}/approve")
def approve_invite(
    invite_id: int,
    background: BackgroundTasks,
    admin: User = Depends(admin_user),
    db: Session = Depends(get_db),
) -> dict:
    inv = _decide(db, invite_id, admin, approved=True)
    _activate_user_by_email(db, inv.email)
    background.add_task(email_mod.send_access_approved, inv.email)
    return _serialize(inv)


@router.post("/invites/{invite_id}/deny")
def deny_invite(
    invite_id: int,
    admin: User = Depends(admin_user),
    db: Session = Depends(get_db),
) -> dict:
    return _serialize(_decide(db, invite_id, admin, approved=False))


@router.post("/invites")
def add_invite(
    payload: dict,
    admin: User = Depends(admin_user),
    db: Session = Depends(get_db),
) -> dict:
    """Manually whitelist an email (approved straight away). Convenience for the admin."""
    email = (payload.get("email") or "").strip().lower()
    if not valid_email(email):
        raise HTTPException(status_code=400, detail="Enter a valid email address.")
    inv = db.execute(select(Invite).where(Invite.email == email)).scalar_one_or_none()
    if inv is None:
        inv = Invite(email=email)
        db.add(inv)
    inv.status = "approved"
    inv.decided_at = datetime.now(timezone.utc)
    inv.decided_by = admin.email
    audit.record(db, admin.id, AuditAction.INVITE_ADD, target_type="invite", target_id=inv.id, summary=email)
    db.commit()
    _activate_user_by_email(db, email)
    return _serialize(inv)


# --- Users & roles --------------------------------------------------------------------

def _serialize_user(u: User) -> dict:
    return {
        "id": u.id,
        "email": u.email,
        "display_name": u.display_name,
        "role": u.role,
        "is_active": u.is_active,
        "is_suspended": u.is_suspended,
        "email_verified": u.email_verified,
        "is_demo": u.is_demo,
        "has_password": u.password_hash is not None,
        "has_google": u.google_sub is not None,
        "created_at": u.created_at.isoformat() if u.created_at else None,
    }


@router.get("/users")
def list_users(_: User = Depends(admin_user), db: Session = Depends(get_db)) -> list[dict]:
    rows = db.execute(select(User).order_by(User.created_at.desc())).scalars().all()
    return [_serialize_user(u) for u in rows]


@router.patch("/users/{user_id}/role")
def set_user_role(
    user_id: int,
    payload: dict,
    background: BackgroundTasks,
    admin: User = Depends(admin_user),
    db: Session = Depends(get_db),
) -> dict:
    """Promote/demote a user. Guards: can't change your own role, can't touch the demo
    account, and can't remove the last remaining admin. Emails the user when it actually changes."""
    role = payload.get("role")
    if role not in ("user", "admin"):
        raise HTTPException(status_code=400, detail="role must be 'user' or 'admin'")
    target = db.get(User, user_id)
    if target is None:
        raise HTTPException(status_code=404, detail="No such user")
    if target.is_demo:
        raise HTTPException(status_code=400, detail="The demo account's role can't be changed.")
    if target.id == admin.id:
        raise HTTPException(status_code=400, detail="You can't change your own role.")
    if (
        target.role == "admin"
        and role == "user"
        and not target.is_suspended
        and count_admins(db, active_only=True) <= 1
    ):
        raise HTTPException(status_code=400, detail="Can't remove the last admin.")
    changed = target.role != role
    if changed:
        audit.record(
            db, admin.id, AuditAction.USER_ROLE_CHANGE,
            target_type="user", target_id=target.id, summary=target.email,
            before={"role": target.role}, after={"role": role},
        )
    target.role = role
    db.commit()
    if changed:
        background.add_task(email_mod.send_role_changed, target.email, role, operator_contact())
    return _serialize_user(target)


@router.patch("/users/{user_id}/suspend")
def set_user_suspended(
    user_id: int,
    payload: dict,
    background: BackgroundTasks,
    admin: User = Depends(admin_user),
    db: Session = Depends(get_db),
) -> dict:
    """Suspend or un-suspend an account. A suspended user can't sign in by any method and any
    live session is dropped on its next request. Guards: can't suspend the demo account, can't
    suspend yourself, and can't suspend the last active admin (that would lock admin out). On
    un-suspend the user is emailed they can return (suspend is announced reactively, on a blocked
    sign-in)."""
    suspended = bool(payload.get("suspended"))
    target = db.get(User, user_id)
    if target is None:
        raise HTTPException(status_code=404, detail="No such user")
    if target.is_demo:
        raise HTTPException(status_code=400, detail="The demo account can't be suspended.")
    if target.id == admin.id:
        raise HTTPException(status_code=400, detail="You can't suspend your own account.")
    if (
        suspended
        and target.role == "admin"
        and not target.is_suspended
        and count_admins(db, active_only=True) <= 1
    ):
        raise HTTPException(status_code=400, detail="Can't suspend the last active admin.")
    was_suspended = target.is_suspended
    if was_suspended != suspended:
        audit.record(
            db, admin.id,
            AuditAction.USER_SUSPEND if suspended else AuditAction.USER_UNSUSPEND,
            target_type="user", target_id=target.id, summary=target.email,
        )
    target.is_suspended = suspended
    db.commit()
    if was_suspended and not suspended:
        background.add_task(
            email_mod.send_account_unsuspended, target.email, operator_contact()
        )
    return _serialize_user(target)


@router.delete("/users/{user_id}")
def admin_delete_user(
    user_id: int,
    background: BackgroundTasks,
    admin: User = Depends(admin_user),
    db: Session = Depends(get_db),
) -> dict:
    """Admin-triggered account deletion — the same full erasure a user can perform on themselves
    (cascade delete of all personal data + access-request cleanup + Google-grant revoke). Guards:
    not the demo account, not yourself (use Settings → Account), and not the last admin."""
    target = db.get(User, user_id)
    if target is None:
        raise HTTPException(status_code=404, detail="No such user")
    if target.is_demo:
        raise HTTPException(status_code=400, detail="The demo account can't be deleted.")
    if target.id == admin.id:
        raise HTTPException(
            status_code=400, detail="Delete your own account from Settings → Account."
        )
    if target.role == "admin" and not target.is_suspended and count_admins(db, active_only=True) <= 1:
        raise HTTPException(status_code=400, detail="Can't delete the last admin.")
    # Record BEFORE the purge — the target row (and its email) is gone afterwards.
    audit.record(
        db, admin.id, AuditAction.USER_DELETE,
        target_type="user", target_id=target.id, summary=target.email,
    )
    purge_user(db, target, background)
    return {"deleted": user_id}


# --- Demo account: email whitelist + state reset -------------------------------------

def _serialize_demo(row: DemoWhitelist) -> dict:
    return {
        "id": row.id,
        "email": row.email,
        "note": row.note,
        "added_by": row.added_by,
        "created_at": row.created_at.isoformat() if row.created_at else None,
    }


@router.get("/demo/whitelist")
def list_demo_whitelist(
    _: User = Depends(admin_user), db: Session = Depends(get_db)
) -> list[dict]:
    rows = (
        db.execute(select(DemoWhitelist).order_by(DemoWhitelist.created_at.desc()))
        .scalars()
        .all()
    )
    return [_serialize_demo(r) for r in rows]


@router.post("/demo/whitelist")
def add_demo_whitelist(
    payload: dict,
    admin: User = Depends(admin_user),
    db: Session = Depends(get_db),
) -> dict:
    """Add an email that may enter the shared demo account from the login page (no Google
    sign-in). Idempotent — re-adding an existing email just returns it."""
    email = (payload.get("email") or "").strip().lower()
    if not valid_email(email):
        raise HTTPException(status_code=400, detail="Enter a valid email address.")
    row = db.execute(
        select(DemoWhitelist).where(DemoWhitelist.email == email)
    ).scalar_one_or_none()
    if row is None:
        row = DemoWhitelist(
            email=email,
            note=(payload.get("note") or "").strip() or None,
            added_by=admin.email,
        )
        db.add(row)
        audit.record(db, admin.id, AuditAction.DEMO_ADD, target_type="demo", target_id=email, summary=email)
        db.commit()
    return _serialize_demo(row)


@router.delete("/demo/whitelist/{entry_id}")
def remove_demo_whitelist(
    entry_id: int,
    admin: User = Depends(admin_user),
    db: Session = Depends(get_db),
) -> dict:
    row = db.get(DemoWhitelist, entry_id)
    if row is not None:
        audit.record(db, admin.id, AuditAction.DEMO_REMOVE, target_type="demo", target_id=row.email, summary=row.email)
        db.delete(row)
        db.commit()
    return {"deleted": entry_id}


def _seed_demo_playlists(db: Session, demo: User) -> int:
    """Seed a few sample playlists from the shared catalog so a freshly-reset demo isn't
    empty — gives visitors something to play with. Best-effort: skips silently if the
    catalog is too small."""
    recent = (
        db.execute(
            select(Video.id)
            .where(Video.is_short.is_(False), Video.live_status == "none")
            .order_by(Video.published_at.desc().nullslast())
            .limit(60)
        )
        .scalars()
        .all()
    )
    if not recent:
        return 0
    samples = [
        ("Fresh picks", recent[0:10]),
        ("Quick watch", recent[10:18]),
        ("Saved for later", recent[18:26]),
    ]
    created = 0
    for pos, (name, vids) in enumerate(samples):
        if not vids:
            continue
        pl = Playlist(user_id=demo.id, name=name, position=pos)
        db.add(pl)
        db.flush()
        for ipos, vid in enumerate(vids):
            db.add(PlaylistItem(playlist_id=pl.id, video_id=vid, position=ipos))
        created += 1
    return created


def _seed_demo_subscriptions(db: Session, demo: User, n: int = 14) -> int:
    """Seed the demo with a handful of catalog channels (the ones with the most videos) so its
    Channel manager isn't empty — gives visitors something to explore and matches the landing-page
    screenshot. Idempotent: clears the demo's subscriptions first, then re-adds the top N."""
    db.execute(delete(Subscription).where(Subscription.user_id == demo.id))
    top = db.execute(
        select(Video.channel_id)
        .where(Video.channel_id.is_not(None))
        .group_by(Video.channel_id)
        .order_by(func.count().desc())
        .limit(n)
    ).scalars().all()
    for channel_id in top:
        db.add(Subscription(user_id=demo.id, channel_id=channel_id))
    return len(top)


def reset_demo_state(db: Session, demo: User) -> int:
    """Wipe the demo account's per-user state (watch/save/hide states, playlists, preferences)
    back to a clean baseline and re-seed a few sample playlists + channel subscriptions. Returns
    the seeded playlist count. Shared by the admin's manual reset button and the scheduled demo
    reset (see scheduler)."""
    db.execute(delete(VideoState).where(VideoState.user_id == demo.id))
    # Playlist items cascade on playlist delete (ondelete="CASCADE").
    db.execute(delete(Playlist).where(Playlist.user_id == demo.id))
    demo.preferences = {"language": "en"}
    db.commit()
    seeded = _seed_demo_playlists(db, demo)
    _seed_demo_subscriptions(db, demo)
    db.commit()
    return seeded


@router.post("/demo/reset")
def reset_demo(
    admin: User = Depends(admin_user), db: Session = Depends(get_db)
) -> dict:
    """Manually clean up the shared demo sandbox back to a baseline. A scheduled job does the
    same automatically (admin-tunable interval); this is the admin's on-demand button."""
    demo = get_or_create_demo_user(db)
    seeded = reset_demo_state(db, demo)
    audit.record(db, admin.id, AuditAction.DEMO_RESET, summary=f"{seeded} sample playlists re-seeded")
    db.commit()
    return {"reset": True, "playlists_seeded": seeded}


@router.post("/purge-discovery")
def purge_discovery(
    admin: User = Depends(admin_user), db: Session = Depends(get_db)
) -> dict:
    """Reclaim all un-kept discovery content NOW (ignoring the grace period): live-search result
    videos nobody watched/saved/subscribed to, plus explored-but-unsubscribed channels. The
    scheduled discovery-cleanup job does the same on its interval; this is the on-demand button."""
    from app.sync.explore import purge_unkept_explored, purge_unkept_search

    result = {**purge_unkept_search(db, grace_days=0), **purge_unkept_explored(db)}
    audit.record(
        db, admin.id, AuditAction.DISCOVERY_PURGE,
        summary=", ".join(f"{k}: {v}" for k, v in result.items()) or None, after=result,
    )
    db.commit()
    return result
