from __future__ import annotations

from datetime import date, datetime, timezone

from fastapi import HTTPException, Request
from sqlalchemy import or_, select
from sqlalchemy.orm import Session

from app.core.security.passwords import verify_password
from app.modules.clients.models import Client
from app.modules.core.iam.models import User
from app.modules.core.rbac.deps import get_user_roles
from app.modules.credential_vault.crypto import decrypt_value, encrypt_value
from app.modules.credential_vault.models import CredentialVaultAccessLog, CredentialVaultEntry, CredentialVaultVersion

MANAGER_ROLES = {"Firm Admin", "Partner", "Branch Manager"}
DENIED_ROLES = {"Client", "Consultant"}


def utcnow() -> datetime:
    return datetime.now(timezone.utc)


def active_tenant_id(request: Request, user: User) -> int:
    return int(request.session.get("active_tenant_id") or user.tenant_id)


def active_branch_id(request: Request, user: User) -> int | None:
    raw = request.session.get("active_branch_id")
    return int(raw) if raw not in (None, "", 0, "0") else None


def role_names(db: Session, user: User) -> set[str]:
    return set(get_user_roles(db, user.id))


def can_open_vault(db: Session, user: User) -> bool:
    roles = role_names(db, user)
    return bool(roles & MANAGER_ROLES) or "Staff" in roles or "Employee" in roles


def can_manage_vault(db: Session, user: User) -> bool:
    return bool(role_names(db, user) & MANAGER_ROLES)


def _allowed_ids(entry: CredentialVaultEntry) -> set[int]:
    result: set[int] = set()
    for raw in (entry.allowed_user_ids_csv or "").split(","):
        try:
            result.add(int(raw.strip()))
        except (TypeError, ValueError):
            pass
    return result


def can_view_entry(db: Session, user: User, entry: CredentialVaultEntry, branch_id: int | None = None) -> bool:
    roles = role_names(db, user)
    if roles & DENIED_ROLES:
        return False
    if entry.tenant_id != user.tenant_id and "System Admin" not in roles:
        return False
    if "Firm Admin" in roles or "Partner" in roles:
        return True
    if "Branch Manager" in roles:
        return not entry.branch_id or entry.branch_id == (branch_id or user.branch_id)
    return entry.owner_user_id == user.id or user.id in _allowed_ids(entry)


def list_visible_entries(db: Session, user: User, tenant_id: int, branch_id: int | None, include_archived: bool = False):
    q = select(CredentialVaultEntry, Client).outerjoin(Client, Client.id == CredentialVaultEntry.client_id).where(CredentialVaultEntry.tenant_id == tenant_id)
    if not include_archived:
        q = q.where(CredentialVaultEntry.status != "archived")
    if branch_id:
        q = q.where(or_(CredentialVaultEntry.branch_id.is_(None), CredentialVaultEntry.branch_id == branch_id))
    rows = db.execute(q.order_by(CredentialVaultEntry.rotation_due_on.asc().nullslast(), CredentialVaultEntry.title)).all()
    return [(entry, client) for entry, client in rows if can_view_entry(db, user, entry, branch_id)]


def log_access(db: Session, request: Request, user: User, entry: CredentialVaultEntry | None, action: str, *, reason: str | None = None, fields: str | None = None, success: bool = True) -> None:
    db.add(CredentialVaultAccessLog(
        tenant_id=entry.tenant_id if entry else user.tenant_id,
        branch_id=entry.branch_id if entry else user.branch_id,
        entry_id=entry.id if entry else None,
        actor_user_id=user.id,
        action=action,
        reason=reason,
        fields_accessed_csv=fields,
        success=success,
        ip_address=request.client.host if request.client else None,
        user_agent=request.headers.get("user-agent"),
    ))


def create_entry(db: Session, *, tenant_id: int, branch_id: int | None, client_id: int | None, registration_id: int | None, title: str, category: str, portal_url: str | None, reference_number: str | None, username: str | None, secret: str, additional_secret: str | None, notes: str | None, sensitivity: str, expires_on: date | None, rotation_due_on: date | None, owner_user_id: int | None, allowed_user_ids_csv: str | None, actor_user_id: int) -> CredentialVaultEntry:
    if not secret.strip():
        raise HTTPException(400, "Password, token or secret is required.")
    entry = CredentialVaultEntry(
        tenant_id=tenant_id, branch_id=branch_id, client_id=client_id, registration_id=registration_id,
        title=title.strip(), category=category, portal_url=portal_url or None, reference_number=reference_number or None,
        username_encrypted=encrypt_value(tenant_id, username), secret_encrypted=encrypt_value(tenant_id, secret) or "",
        additional_secret_encrypted=encrypt_value(tenant_id, additional_secret), notes_encrypted=encrypt_value(tenant_id, notes),
        sensitivity=sensitivity, expires_on=expires_on, rotation_due_on=rotation_due_on, owner_user_id=owner_user_id,
        allowed_user_ids_csv=allowed_user_ids_csv or None, created_by_user_id=actor_user_id, updated_by_user_id=actor_user_id,
    )
    db.add(entry)
    db.flush()
    return entry


def reveal_entry(db: Session, request: Request, user: User, entry: CredentialVaultEntry, current_password: str, reason: str) -> dict[str, str]:
    if not can_view_entry(db, user, entry, active_branch_id(request, user)):
        log_access(db, request, user, entry, "reveal", reason=reason, success=False); db.commit()
        raise HTTPException(403, "You are not authorised to reveal this credential.")
    if not reason.strip():
        raise HTTPException(400, "A business reason is required.")
    if not verify_password(current_password, user.password_hash):
        log_access(db, request, user, entry, "reveal", reason=reason, success=False); db.commit()
        raise HTTPException(403, "Current ERP password is incorrect.")
    values = {
        "username": decrypt_value(entry.tenant_id, entry.username_encrypted),
        "secret": decrypt_value(entry.tenant_id, entry.secret_encrypted),
        "additional_secret": decrypt_value(entry.tenant_id, entry.additional_secret_encrypted),
        "notes": decrypt_value(entry.tenant_id, entry.notes_encrypted),
    }
    log_access(db, request, user, entry, "reveal", reason=reason.strip(), fields="username,secret,additional_secret,notes", success=True)
    db.commit()
    return values


def rotate_entry(db: Session, entry: CredentialVaultEntry, *, username: str | None, secret: str, additional_secret: str | None, notes: str | None, rotation_due_on: date | None, reason: str, actor_user_id: int) -> None:
    if not secret.strip() or not reason.strip():
        raise HTTPException(400, "New secret and rotation reason are required.")
    version_count = db.execute(select(CredentialVaultVersion).where(CredentialVaultVersion.entry_id == entry.id)).scalars().all()
    db.add(CredentialVaultVersion(
        tenant_id=entry.tenant_id, entry_id=entry.id, version_number=len(version_count) + 1,
        username_encrypted=entry.username_encrypted, secret_encrypted=entry.secret_encrypted,
        additional_secret_encrypted=entry.additional_secret_encrypted, notes_encrypted=entry.notes_encrypted,
        change_reason=reason.strip(), changed_by_user_id=actor_user_id,
    ))
    entry.username_encrypted = encrypt_value(entry.tenant_id, username)
    entry.secret_encrypted = encrypt_value(entry.tenant_id, secret) or ""
    entry.additional_secret_encrypted = encrypt_value(entry.tenant_id, additional_secret)
    entry.notes_encrypted = encrypt_value(entry.tenant_id, notes)
    entry.rotation_due_on = rotation_due_on
    entry.last_rotated_at_utc = utcnow()
    entry.updated_by_user_id = actor_user_id


def due_state(entry: CredentialVaultEntry) -> str:
    today = date.today()
    due = entry.rotation_due_on or entry.expires_on
    if not due:
        return "not_scheduled"
    days = (due - today).days
    if days < 0:
        return "overdue"
    if days <= 30:
        return "due_soon"
    return "current"
