from datetime import date, datetime, timezone
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.api.workspace import get_verified_current_user
from app.core.permissions import Permission
from app.db.session import get_db
from app.models.expense_income import Expense
from app.models.payroll import PayrollProfile, PayrollRun
from app.models.user import User
from app.schemas.payroll import PayrollPay, PayrollProfileCreate, PayrollProfileRead, PayrollProfileUpdate, PayrollRunCreate, PayrollRunRead, PayrollRunUpdate
from app.services.payroll import PayrollError, create_run, ensure_salary_category, member, profile, run
from app.services.rbac import require_permission
from app.services.audit import record_audit
router=APIRouter(tags=["Payroll"])
def fail(exc):
    if isinstance(exc, LookupError): raise HTTPException(404,str(exc))
    if isinstance(exc, PayrollError): raise HTTPException(422,str(exc))
    raise exc
@router.post('/api/businesses/{business_id}/payroll/profiles',response_model=PayrollProfileRead,status_code=201)
def create_profile(business_id:UUID,data:PayrollProfileCreate,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.MANAGE_PAYROLL_PROFILES)
    try:
        target=member(db,business_id,data.membership_id)
        if not target: raise LookupError("Staff membership not found.")
        existing=db.scalar(select(PayrollProfile).where(PayrollProfile.business_id==business_id,PayrollProfile.membership_id==target.id,PayrollProfile.is_active.is_(True)))
        if existing: raise PayrollError("An active payroll profile already exists for this staff member.")
        obj=PayrollProfile(business_id=business_id,**data.model_dump()); db.add(obj); db.commit(); db.refresh(obj); return obj
    except Exception as e: db.rollback(); fail(e)
@router.get('/api/businesses/{business_id}/payroll/profiles',response_model=list[PayrollProfileRead])
def list_profiles(business_id:UUID,membership_id:UUID|None=None,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.PROCESS_PAYROLL); q=select(PayrollProfile).where(PayrollProfile.business_id==business_id); return list(db.scalars(q.where(PayrollProfile.membership_id==membership_id) if membership_id else q))
@router.get('/api/businesses/{business_id}/payroll/profiles/{profile_id}',response_model=PayrollProfileRead)
def get_profile(business_id:UUID,profile_id:UUID,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.PROCESS_PAYROLL); obj=profile(db,business_id,profile_id)
    if not obj: raise HTTPException(404,"Payroll profile not found.")
    return obj
@router.patch('/api/businesses/{business_id}/payroll/profiles/{profile_id}',response_model=PayrollProfileRead)
def update_profile(business_id:UUID,profile_id:UUID,data:PayrollProfileUpdate,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.MANAGE_PAYROLL_PROFILES)
    try:
        obj=profile(db,business_id,profile_id,True)
        if not obj: raise LookupError("Payroll profile not found.")
        for k,v in data.model_dump(exclude_unset=True).items(): setattr(obj,k,v)
        db.commit(); db.refresh(obj); return obj
    except Exception as e: db.rollback(); fail(e)
@router.post('/api/businesses/{business_id}/payroll/runs',response_model=PayrollRunRead,status_code=201)
def new_run(business_id:UUID,data:PayrollRunCreate,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.PROCESS_PAYROLL)
    try: return create_run(db,business_id,user.id,data)
    except IntegrityError: db.rollback(); raise HTTPException(409,"An active payroll run already exists for this staff member and period.")
    except Exception as e: db.rollback(); fail(e)
@router.get('/api/businesses/{business_id}/payroll/runs',response_model=list[PayrollRunRead])
def list_runs(business_id:UUID,membership_id:UUID|None=None,status_filter:str|None=None,start_date:date|None=None,end_date:date|None=None,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.PROCESS_PAYROLL); q=select(PayrollRun).where(PayrollRun.business_id==business_id)
    if membership_id:q=q.where(PayrollRun.membership_id==membership_id)
    if status_filter:q=q.where(PayrollRun.status==status_filter.upper())
    if start_date:q=q.where(PayrollRun.period_end>=start_date)
    if end_date:q=q.where(PayrollRun.period_start<=end_date)
    return list(db.scalars(q.order_by(PayrollRun.created_at.desc())))
@router.get('/api/businesses/{business_id}/payroll/runs/{run_id}',response_model=PayrollRunRead)
def get_run(business_id:UUID,run_id:UUID,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.PROCESS_PAYROLL); obj=run(db,business_id,run_id)
    if not obj:raise HTTPException(404,"Payroll run not found.")
    return obj
@router.patch('/api/businesses/{business_id}/payroll/runs/{run_id}',response_model=PayrollRunRead)
def edit_run(business_id:UUID,run_id:UUID,data:PayrollRunUpdate,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.PROCESS_PAYROLL)
    try:
        obj=run(db,business_id,run_id,True)
        if not obj:raise LookupError("Payroll run not found.")
        if obj.status!="DRAFT":raise PayrollError("Only draft payroll runs can be edited.")
        values=data.model_dump(exclude_unset=True)
        for k,v in values.items():setattr(obj,k,v)
        if obj.period_end<obj.period_start:raise PayrollError("Period end cannot be before period start.")
        if obj.adjustment_amount!=0 and not (obj.adjustment_note or '').strip():raise PayrollError("An adjustment note is required for non-zero adjustments.")
        obj.total_amount=obj.base_salary+obj.adjustment_amount
        if obj.total_amount<0:raise PayrollError("Payroll total cannot be negative.")
        db.commit();db.refresh(obj);return obj
    except Exception as e:db.rollback();fail(e)
@router.post('/api/businesses/{business_id}/payroll/runs/{run_id}/approve',response_model=PayrollRunRead)
def approve(business_id:UUID,run_id:UUID,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.APPROVE_PAYROLL)
    try:
        obj=run(db,business_id,run_id,True)
        if not obj:raise LookupError("Payroll run not found.")
        if obj.status!='DRAFT':raise PayrollError("Only draft payroll runs can be approved.")
        obj.status='APPROVED';obj.approved_by=user.id;obj.approved_at=datetime.now(timezone.utc)
        record_audit(db,business_id,'PAYROLL_APPROVED','payroll_run',obj.id,user.id,None,{
            'payroll_run_id':str(obj.id),'period_start':obj.period_start.isoformat(),
            'period_end':obj.period_end.isoformat(),'amount':str(obj.total_amount),
            'previous_status':'DRAFT','new_status':'APPROVED',
        })
        db.commit();db.refresh(obj);return obj
    except Exception as e:db.rollback();fail(e)
@router.post('/api/businesses/{business_id}/payroll/runs/{run_id}/pay',response_model=PayrollRunRead)
def pay(business_id:UUID,run_id:UUID,data:PayrollPay,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.PAY_PAYROLL)
    try:
        obj=run(db,business_id,run_id,True)
        if not obj:raise LookupError("Payroll run not found.")
        if obj.status!='APPROVED':raise PayrollError("Only approved payroll runs can be paid.")
        if not member(db,business_id,obj.membership_id):raise PayrollError("Payroll staff membership is invalid.")
        category=ensure_salary_category(db,business_id)
        expense=Expense(business_id=business_id,branch_id=None,category_id=category.id,description=f"Salary payroll for {obj.membership_id} ({obj.period_start} to {obj.period_end})",amount=obj.total_amount,expense_date=date.today(),payment_method=data.payment_method,recorded_by=user.id,status='ACTIVE')
        db.add(expense);db.flush();obj.expense_id=expense.id;obj.payment_method=data.payment_method;obj.status='PAID';obj.paid_by=user.id;obj.paid_at=datetime.now(timezone.utc)
        record_audit(db,business_id,'PAYROLL_PAID','payroll_run',obj.id,user.id,None,{
            'payroll_run_id':str(obj.id),'period_start':obj.period_start.isoformat(),
            'period_end':obj.period_end.isoformat(),'amount':str(obj.total_amount),
            'payment_method':data.payment_method,'previous_status':'APPROVED','new_status':'PAID',
        })
        db.commit();db.refresh(obj);return obj
    except Exception as e:db.rollback();fail(e)
@router.post('/api/businesses/{business_id}/payroll/runs/{run_id}/cancel',response_model=PayrollRunRead)
def cancel(business_id:UUID,run_id:UUID,user:User=Depends(get_verified_current_user),db:Session=Depends(get_db)):
    require_permission(db,user,business_id,Permission.PROCESS_PAYROLL)
    try:
        obj=run(db,business_id,run_id,True)
        if not obj:raise LookupError("Payroll run not found.")
        if obj.status=='PAID' or obj.expense_id:raise PayrollError("Paid payroll cannot be cancelled.")
        if obj.status=='CANCELLED':raise PayrollError("Payroll run is already cancelled.")
        previous_status=obj.status
        obj.status='CANCELLED'
        record_audit(db,business_id,'PAYROLL_CANCELLED','payroll_run',obj.id,user.id,None,{
            'payroll_run_id':str(obj.id),'period_start':obj.period_start.isoformat(),
            'period_end':obj.period_end.isoformat(),'amount':str(obj.total_amount),
            'previous_status':previous_status,'new_status':'CANCELLED',
        })
        db.commit();db.refresh(obj);return obj
    except Exception as e:db.rollback();fail(e)
