# ============================================================
# Security module — JWT / password hashing
# ============================================================
import os
from datetime import datetime, timedelta, timezone
from typing import Annotated, Any

import bcrypt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from sqlalchemy.orm import Session

from app.core.database import get_db
from app.core.models import Tenant, TenantStatus, User, UserRole, UserStatus


ROLE_PERMISSION_TEMPLATES: dict[UserRole, set[str]] = {
    UserRole.admin: {"*"},
    UserRole.staff: {
        "dashboard.read",
        "customers.read", "customers.write",
        "products.read", "products.write",
        "employees.read", "employees.write",
        "invoices.read", "invoices.write", "invoices.send",
        "subscriptions.read", "subscriptions.write",
        "expenses.read", "expenses.write", "expenses.confirm",
        "receipts.read", "receipts.write",
        "payroll.read", "payroll.write",
        "finance.read",
        "gst.read", "gst.write",
        "reminders.read", "reminders.write",
        "email_templates.read", "email_templates.write",
        "ai_chat.read",
        "settings.read",
    },
    UserRole.viewer: {
        "dashboard.read",
        "customers.read",
        "products.read",
        "employees.read",
        "invoices.read",
        "subscriptions.read",
        "expenses.read",
        "receipts.read",
        "payroll.read",
        "finance.read",
        "gst.read",
        "reminders.read",
        "email_templates.read",
        "ai_chat.read",
        "settings.read",
    },
}


def get_permission_catalog() -> list[str]:
    permissions = {
        permission
        for permission_set in ROLE_PERMISSION_TEMPLATES.values()
        for permission in permission_set
        if permission != "*"
    }
    return sorted(permissions)


def _normalized_list(values: Any) -> list[str]:
    if not values:
        return []
    if isinstance(values, str):
        return []
    return [str(value) for value in values]


def get_effective_permissions(user: Any) -> set[str]:
    tenant_role = getattr(user, "tenant_role", None)
    base = set(getattr(tenant_role, "permissions_json", None) or ROLE_PERMISSION_TEMPLATES.get(user.role, set()))
    allowed_overrides = _normalized_list(getattr(user, "allowed_permissions_json", None))
    denied_overrides = set(_normalized_list(getattr(user, "denied_permissions_json", None)))

    if "*" in base:
        base.update(allowed_overrides)
        return base.difference(denied_overrides)

    base.update(allowed_overrides)
    base.difference_update(denied_overrides)
    return base


def get_allowed_routes(user: Any) -> list[str]:
    return _normalized_list(getattr(user, "allowed_routes_json", None))


def get_client_permissions(user: Any) -> list[str]:
    permissions = get_effective_permissions(user)
    denied_overrides = set(_normalized_list(getattr(user, "denied_permissions_json", None)))
    client_permissions = {permission for permission in permissions if permission != "*"}

    if "*" in permissions:
        client_permissions.update(get_permission_catalog())

    client_permissions.difference_update(denied_overrides)
    return sorted(client_permissions)


def has_permission(user: Any, permission: str) -> bool:
    permissions = get_effective_permissions(user)
    denied_overrides = set(_normalized_list(getattr(user, "denied_permissions_json", None)))
    if permission in denied_overrides:
        return False
    return "*" in permissions or permission in permissions


# -------------------- Password hashing --------------------
def hash_password(password: str) -> str:
    """Hash a password using bcrypt."""
    return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")


def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verify a password against its hash."""
    return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))


# -------------------- JWT helpers --------------------
JWT_SECRET = os.getenv("JWT_SECRET", "")
if not JWT_SECRET or len(JWT_SECRET) < 32:
    raise RuntimeError("JWT_SECRET must be set to a strong value of at least 32 characters")

JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
JWT_EXPIRE_MINUTES = int(os.getenv("JWT_EXPIRE_MINUTES", "1440"))  # 24 hours default

ALGORITHM = JWT_ALGORITHM
PLATFORM_TENANT_CODE = os.getenv("PLATFORM_TENANT_CODE", "platform")


def create_jwt_token(payload: dict[str, Any]) -> str:
    """
    Create a JWT token.

    Args:
        payload: Must contain at least {"sub": user_id, "username": ..., "role": ...}
    """
    to_encode = payload.copy()
    expire = datetime.now(timezone.utc) + timedelta(minutes=JWT_EXPIRE_MINUTES)
    to_encode.update({
        "exp": expire,
        "iat": datetime.now(timezone.utc),
    })
    return jwt.encode(to_encode, JWT_SECRET, algorithm=ALGORITHM)


def decode_jwt_token(token: str) -> dict[str, Any]:
    """
    Decode and validate a JWT token.
    Raises HTTPException on failure.
    """
    credentials_exception = HTTPException(
        status_code=401,
        detail="Invalid or expired token",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM])
        return payload
    except JWTError:
        raise credentials_exception


# -------------------- Auth dependency --------------------
security = HTTPBearer()


async def get_current_user(
    credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
    db: Session = Depends(get_db),
) -> User:
    """
    Validate Bearer JWT token, query User from DB, check status.
    All business API endpoints depend on this.
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid or expired token",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = decode_jwt_token(credentials.credentials)
    except HTTPException:
        raise credentials_exception

    user_id = payload.get("sub")
    tenant_id = payload.get("tenant_id")
    if user_id is None or tenant_id is None:
        raise credentials_exception
    try:
        user_id = int(user_id)
        tenant_id = int(tenant_id)
    except (TypeError, ValueError):
        raise credentials_exception

    from sqlalchemy import select

    user = db.execute(
        select(User).where(User.id == user_id, User.tenant_id == tenant_id)
    ).scalar_one_or_none()

    if user is None:
        raise credentials_exception

    if user.status != UserStatus.active:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="User account is disabled",
        )

    tenant_status = db.execute(
        select(Tenant.status).where(Tenant.id == tenant_id)
    ).scalar_one_or_none()
    if tenant_status != TenantStatus.active:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Tenant account is disabled",
        )

    return user


def require_permission(permission: str):
    async def dependency(current_user: User = Depends(get_current_user)) -> User:
        if not has_permission(current_user, permission):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Permission required: {permission}",
            )
        return current_user

    return dependency


async def require_admin(current_user: User = Depends(get_current_user)) -> User:
    """Require an authenticated admin user."""
    if current_user.role != UserRole.admin:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Admin privileges required",
        )
    return current_user


async def require_platform_admin(
    credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
    db: Session = Depends(get_db),
) -> User:
    """Require admin role AND membership in the platform tenant."""
    user = await get_current_user(credentials, db)
    if user.role != UserRole.admin:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Admin privileges required",
        )
    from app.core.models import Tenant
    tenant = db.get(Tenant, user.tenant_id)
    if not tenant or tenant.company_code != PLATFORM_TENANT_CODE:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Platform admin privileges required",
        )
    return user
