# app/core/security.py
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
import bcrypt
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.core.config import settings
 
bearer_scheme = HTTPBearer()
 
def hash_password(password: str) -> str:
    """
    Hashes a plain text password using bcrypt before storing it in the database.
    """
    salt = bcrypt.gensalt()
    return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
 
def verify_password(plain: str, hashed: str) -> bool:
    """
    Verifies a plain text password against its bcrypt hashed version during login.
    """
    return bcrypt.checkpw(plain.encode('utf-8'), hashed.encode('utf-8'))
 
def create_access_token(data: dict) -> str:
    """
    Generates a secure JSON Web Token (JWT) for an authenticated user.
    Includes an expiration time to limit the lifespan of the session.
    """
    expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    return jwt.encode({**data, 'exp': expire}, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
 
def decode_token(token: str) -> dict:
    """
    Validates and decodes a JWT. 
    Raises a 401 Unauthorized exception if the token is tampered with or expired.
    """
    try:
        return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
    except JWTError:
        raise HTTPException(status_code=401, detail='Invalid or expired token')
 
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.models.core_models import LoginUsers
from app.models.access_models import ModuleMaster, RoleModulePermission, UserPermissionOverride

async def get_current_user(
    creds: HTTPAuthorizationCredentials = Depends(bearer_scheme),
    db: AsyncSession = Depends(get_db)
):
    """
    FastAPI Dependency to secure endpoints.
    Purpose: Extracts the Bearer token from the request header, decodes it,
    fetches the full user object from the database, and returns it.
    """
    payload = decode_token(creds.credentials)
    uid = payload.get('user_id') or payload.get('sub')
    user = await db.get(LoginUsers, uid)
    if not user or not user.is_active:
        raise HTTPException(status_code=401, detail='User not found or inactive')
    return user

async def user_has_permission(db: AsyncSession, user: LoginUsers, module_code: str, action: str) -> bool:
    """
    Checks whether `user` may perform `action` (view/create/edit/delete/approve) on `module_code`.
    Purely data-driven, resolved by role_code — never by username or any other identity check:
    a per-user override (if set for that flag) wins, else it falls back to the user's role's row
    in the permission matrix. There is no code-level ADMIN bypass here — ADMIN gets access the
    same way every other role does, via real role_module_permission rows (seeded True for every
    module). That also means a module with is_active=False blocks everyone, no exceptions.
    """
    module = (await db.execute(
        select(ModuleMaster).where(ModuleMaster.module_code == module_code)
    )).scalar_one_or_none()
    if module is None or not module.is_active:
        return False

    override = (await db.execute(
        select(UserPermissionOverride).where(
            UserPermissionOverride.user_id == user.user_id,
            UserPermissionOverride.module_code == module_code,
        )
    )).scalar_one_or_none()
    if override is not None:
        flag = getattr(override, f'can_{action}')
        if flag is not None:
            return bool(flag)

    role_perm = (await db.execute(
        select(RoleModulePermission).where(
            RoleModulePermission.role_code == user.role_code,
            RoleModulePermission.module_code == module_code,
        )
    )).scalar_one_or_none()
    if role_perm is None:
        return False
    return bool(getattr(role_perm, f'can_{action}'))


async def get_effective_permissions(db: AsyncSession, user: LoginUsers) -> list[dict]:
    """
    The full view/create/edit/delete/approve matrix for `user`, one row per active module,
    resolved the same way as user_has_permission (override wins, else role's matrix row,
    else all-False) — no ADMIN special-casing here either. Used to drive the frontend's own
    nav/action visibility so it never drifts out of sync with what the backend actually
    enforces, and to let a user see exactly what they're permitted to do.
    """
    modules = (await db.execute(
        select(ModuleMaster).where(ModuleMaster.is_active == True).order_by(ModuleMaster.module_name)
    )).scalars().all()

    role_perms = {
        rp.module_code: rp
        for rp in (await db.execute(
            select(RoleModulePermission).where(RoleModulePermission.role_code == user.role_code)
        )).scalars().all()
    }
    overrides = {
        o.module_code: o
        for o in (await db.execute(
            select(UserPermissionOverride).where(UserPermissionOverride.user_id == user.user_id)
        )).scalars().all()
    }

    result = []
    for module in modules:
        role_perm = role_perms.get(module.module_code)
        override = overrides.get(module.module_code)
        perms = {}
        for action in ('view', 'create', 'edit', 'delete', 'approve'):
            flag = getattr(override, f'can_{action}', None) if override else None
            if flag is None:
                flag = bool(getattr(role_perm, f'can_{action}')) if role_perm else False
            perms[f'can_{action}'] = bool(flag)
        result.append({'module_code': module.module_code, 'module_name': module.module_name, **perms})
    return result

def require_permission(module_code: str, action: str):
    """
    FastAPI dependency factory that enforces module-wise, database-backed access control.
    Usage: Depends(require_permission('PROCUREMENT', 'approve'))
    Who can do what is data (role_module_permission / user_permission_override rows),
    not code — change it by editing those tables, no redeploy needed.
    """
    async def guard(
        user: LoginUsers = Depends(get_current_user),
        db: AsyncSession = Depends(get_db),
    ):
        if not await user_has_permission(db, user, module_code, action):
            raise HTTPException(
                status_code=403,
                detail=f"Access denied: role '{user.role_code}' cannot '{action}' on '{module_code}'"
            )
        return user
    return guard

def require_permission_any(module_codes: list[str], action: str):
    """
    FastAPI dependency factory that allows access if the user has permission for ANY of the modules.
    Usage: Depends(require_permission_any(['YARD', 'INVENTORY'], 'view'))
    """
    async def guard(
        user: LoginUsers = Depends(get_current_user),
        db: AsyncSession = Depends(get_db),
    ):
        for module_code in module_codes:
            if await user_has_permission(db, user, module_code, action):
                return user
        raise HTTPException(
            status_code=403,
            detail=f"Access denied: role '{user.role_code}' cannot '{action}' on any of '{module_codes}'"
        )
    return guard
