from datetime import datetime, timezone
from pydantic import BaseModel, Field
from fastapi import APIRouter, Depends, Header, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from croniter import croniter
from app.core.database import get_db
from app.api.deps import current_user
from app.models.entities import (User,Tenant,DataSource,DataFieldMetadata,TenantModuleAssignment,
 VisualGovernancePolicy,DataProcessFlow,DataProcessRun,GovernedReportSchedule,Report,ReportSchedule)
from app.services.saas_tenancy import resolve_membership
from app.services.modular_governance import module_catalog,ensure_module_assignments,discover_and_sync_fields,effective_policies
router=APIRouter(prefix='/api/v024',tags=['V024 Modular SaaS and Visual Governance'])
ADMIN={'admin','director','data_steward','data_engineer'}

def tenant_ctx(x_tenant_id:str=Header(...,alias='X-Tenant-ID'),db:Session=Depends(get_db),user:User=Depends(current_user)):
    tenant=db.get(Tenant,x_tenant_id); membership=resolve_membership(db,user,x_tenant_id) if tenant else None
    if not tenant or tenant.status!='active': raise HTTPException(404,'Tenant ativo não encontrado')
    if getattr(user.role,'value',user.role) not in {'admin','director'} and not membership: raise HTTPException(403,'Usuário não pertence ao tenant')
    return tenant,user

def require_admin(ctx=Depends(tenant_ctx)):
    tenant,user=ctx
    if getattr(user.role,'value',user.role) not in ADMIN: raise HTTPException(403,'Perfil de governança necessário')
    return tenant,user

class ModuleIn(BaseModel): enabled:bool=True; contract_limits:dict={}; configuration:dict={}
class FieldIn(BaseModel): friendly_name:str=''; description:str=''; business_notes:str=''; classification:str='internal'; tags:list=[]; visible:bool=True; searchable:bool=True; aggregatable:bool=True
class PolicyIn(BaseModel): name:str; dataset_id:str; subject_type:str; subject_id:str; effect:str='allow'; actions:list=['read']; allowed_columns:list=[]; denied_columns:list=[]; scope_type:str='all'; scope_config:dict={}; priority:int=100
class FlowIn(BaseModel): name:str; description:str=''; department_id:str|None=None; source_id:str|None=None; nodes:list=[]; edges:list=[]; schedule_cron:str|None=None; timezone:str='America/Sao_Paulo'; enabled:bool=True
class ScheduleIn(BaseModel): report_id:str; name:str; cron:str; timezone:str='America/Sao_Paulo'; formats:list=['pdf']; channels:list=['dashboard']; recipient_user_ids:list=[]; recipient_team_ids:list=[]; recipient_emails:list=[]; subject_template:str='Relatório atualizado'; message_template:str=''

@router.get('/summary')
def summary(ctx=Depends(tenant_ctx),db:Session=Depends(get_db)):
    tenant,user=ctx; ensure_module_assignments(db,tenant.id,user.id)
    return {'version':'V024','tenant_id':tenant.id,'modules':len(module_catalog()),'fields':len(db.scalars(select(DataFieldMetadata).where(DataFieldMetadata.tenant_id==tenant.id)).all()),'policies':len(db.scalars(select(VisualGovernancePolicy).where(VisualGovernancePolicy.tenant_id==tenant.id)).all())}

@router.get('/modules/catalog')
def modules_catalog(): return module_catalog()
@router.get('/modules')
def modules(ctx=Depends(tenant_ctx),db:Session=Depends(get_db)):
    tenant,user=ctx; ensure_module_assignments(db,tenant.id,user.id); rows=db.scalars(select(TenantModuleAssignment).where(TenantModuleAssignment.tenant_id==tenant.id).order_by(TenantModuleAssignment.module_code)).all(); names={x['code']:x['name'] for x in module_catalog()}; return [{'id':x.id,'code':x.module_code,'name':names.get(x.module_code,x.module_code),'enabled':x.enabled,'contract_limits':x.contract_limits,'configuration':x.configuration} for x in rows]
@router.put('/modules/{code}')
def set_module(code:str,p:ModuleIn,ctx=Depends(require_admin),db:Session=Depends(get_db)):
    tenant,user=ctx
    if code not in {x['code'] for x in module_catalog()}: raise HTTPException(404,'Módulo não encontrado')
    x=db.scalar(select(TenantModuleAssignment).where(TenantModuleAssignment.tenant_id==tenant.id,TenantModuleAssignment.module_code==code)) or TenantModuleAssignment(tenant_id=tenant.id,module_code=code,activated_by=user.id)
    x.enabled=p.enabled; x.contract_limits=p.contract_limits; x.configuration=p.configuration; db.add(x); db.commit(); db.refresh(x); return {'id':x.id,'code':x.module_code,'enabled':x.enabled}

@router.post('/sources/{source_id}/discover-fields')
def discover(source_id:str,schema:str|None=None,ctx=Depends(require_admin),db:Session=Depends(get_db)):
    tenant,user=ctx; source=db.get(DataSource,source_id)
    if not source: raise HTTPException(404,'Fonte não encontrada')
    objects,created=discover_and_sync_fields(db,tenant.id,source,schema,user.id); return {'objects':objects,'new_metadata_records':created}
@router.get('/sources/{source_id}/fields')
def fields(source_id:str,qualified_object:str|None=None,ctx=Depends(tenant_ctx),db:Session=Depends(get_db)):
    tenant,_=ctx; q=select(DataFieldMetadata).where(DataFieldMetadata.tenant_id==tenant.id,DataFieldMetadata.source_id==source_id)
    if qualified_object:q=q.where(DataFieldMetadata.qualified_object==qualified_object)
    rows=db.scalars(q.order_by(DataFieldMetadata.qualified_object,DataFieldMetadata.column_name)).all(); return rows
@router.put('/fields/{field_id}')
def update_field(field_id:str,p:FieldIn,ctx=Depends(require_admin),db:Session=Depends(get_db)):
    tenant,user=ctx; x=db.get(DataFieldMetadata,field_id)
    if not x or x.tenant_id!=tenant.id: raise HTTPException(404,'Campo não encontrado')
    for k,v in p.model_dump().items(): setattr(x,k,v)
    x.updated_by=user.id; db.commit(); db.refresh(x); return x

@router.post('/governance/policies')
def create_policy(p:PolicyIn,ctx=Depends(require_admin),db:Session=Depends(get_db)):
    tenant,user=ctx; x=VisualGovernancePolicy(tenant_id=tenant.id,created_by=user.id,**p.model_dump()); db.add(x); db.commit(); db.refresh(x); return x
@router.get('/governance/policies')
def policies(dataset_id:str|None=None,ctx=Depends(tenant_ctx),db:Session=Depends(get_db)):
    tenant,_=ctx; q=select(VisualGovernancePolicy).where(VisualGovernancePolicy.tenant_id==tenant.id)
    if dataset_id:q=q.where(VisualGovernancePolicy.dataset_id==dataset_id)
    return db.scalars(q.order_by(VisualGovernancePolicy.priority)).all()
@router.get('/governance/effective/{dataset_id}')
def effective(dataset_id:str,ctx=Depends(tenant_ctx),db:Session=Depends(get_db)):
    tenant,user=ctx; rows=effective_policies(db,tenant.id,user,dataset_id); return {'dataset_id':dataset_id,'user_id':user.id,'policies':rows}

@router.post('/flows')
def create_flow(p:FlowIn,ctx=Depends(require_admin),db:Session=Depends(get_db)):
    tenant,user=ctx; next_run=croniter(p.schedule_cron,datetime.now(timezone.utc)).get_next(datetime) if p.schedule_cron else None; x=DataProcessFlow(tenant_id=tenant.id,owner_id=user.id,next_run_at=next_run,**p.model_dump()); db.add(x); db.commit(); db.refresh(x); return x
@router.get('/flows')
def flows(ctx=Depends(tenant_ctx),db:Session=Depends(get_db)):
    tenant,_=ctx; return db.scalars(select(DataProcessFlow).where(DataProcessFlow.tenant_id==tenant.id).order_by(DataProcessFlow.created_at.desc())).all()
@router.post('/flows/{flow_id}/run')
def run_flow(flow_id:str,ctx=Depends(require_admin),db:Session=Depends(get_db)):
    tenant,_=ctx; flow=db.get(DataProcessFlow,flow_id)
    if not flow or flow.tenant_id!=tenant.id: raise HTTPException(404,'Fluxo não encontrado')
    run=DataProcessRun(tenant_id=tenant.id,flow_id=flow.id,status='queued',node_status={str(n.get('id')):'queued' for n in flow.nodes}); db.add(run); db.commit(); db.refresh(run); from app.workers.tasks import execute_data_process_flow_v024_task; execute_data_process_flow_v024_task.delay(flow.id,run.id); return run

@router.post('/report-schedules')
def create_schedule(p:ScheduleIn,ctx=Depends(tenant_ctx),db:Session=Depends(get_db)):
    tenant,user=ctx; report=db.get(Report,p.report_id)
    if not report: raise HTTPException(404,'Relatório não encontrado')
    if report.owner_id!=user.id and getattr(user.role,'value',user.role) not in {'admin','director','manager'}: raise HTTPException(403,'Sem permissão para agendar este relatório')
    next_run=croniter(p.cron,datetime.now(timezone.utc)).get_next(datetime); x=GovernedReportSchedule(tenant_id=tenant.id,owner_id=user.id,next_run_at=next_run,**p.model_dump()); db.add(x); recipients=list(dict.fromkeys(p.recipient_emails)); legacy=ReportSchedule(report_id=p.report_id,owner_id=user.id,cron=p.cron,timezone=p.timezone,formats=p.formats,recipients=recipients,subject_template=p.subject_template,body_template=p.message_template or 'Segue o relatório atualizado.',next_run_at=next_run); db.add(legacy); db.commit(); db.refresh(x); return x
@router.get('/report-schedules')
def schedules(ctx=Depends(tenant_ctx),db:Session=Depends(get_db)):
    tenant,user=ctx; role=getattr(user.role,'value',user.role); q=select(GovernedReportSchedule).where(GovernedReportSchedule.tenant_id==tenant.id)
    if role not in {'admin','director','manager'}: q=q.where(GovernedReportSchedule.owner_id==user.id)
    return db.scalars(q.order_by(GovernedReportSchedule.created_at.desc())).all()
