from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.deps import get_db, current_user, require_roles
from app.models.entities import User, Role, ClusterNode, ClusterLease, FailoverEvent, BackupPolicy, BackupRun, DisasterRecoveryPlan, DisasterRecoveryDrill
from app.services.cluster import register_or_heartbeat, acquire_lease, cluster_summary, execute_backup, next_cron

router=APIRouter(prefix='/api/v014',tags=['V014 High Availability'])
ADMIN_ROLES=(Role.admin,Role.director,Role.data_engineer,Role.auditor)

class HeartbeatIn(BaseModel):
    node_name:str; node_type:str='api'; address:str=''; capabilities:list[str]=Field(default_factory=list)
class LeaseIn(BaseModel): name:str; node_id:str; ttl_seconds:int=45
class BackupPolicyIn(BaseModel):
    name:str; resource_type:str='metadata_database'; schedule_cron:str='0 2 * * *'; retention_days:int=30; backup_type:str='logical'; destination_prefix:str='backups/v014'; encryption_required:bool=True; enabled:bool=True
class DRPlanIn(BaseModel):
    name:str; scope:str='platform'; rpo_minutes:int=60; rto_minutes:int=240; primary_region:str='primary'; recovery_region:str='dr'; runbook:list=Field(default_factory=list); validation_checks:list=Field(default_factory=list)
class DRDrillIn(BaseModel): scenario:str; scheduled_at:datetime|None=None
class DRDrillFinish(BaseModel): measured_rpo_minutes:int; measured_rto_minutes:int; results:dict=Field(default_factory=dict)

@router.get('/summary')
def summary(db:Session=Depends(get_db),user:User=Depends(current_user)):
    data=cluster_summary(db)
    data['backup_policies']=len(db.scalars(select(BackupPolicy)).all())
    data['failed_backups']=len(db.scalars(select(BackupRun).where(BackupRun.status=='failed')).all())
    data['dr_plans']=len(db.scalars(select(DisasterRecoveryPlan)).all())
    return data

@router.post('/nodes/heartbeat')
def heartbeat(p:HeartbeatIn,db:Session=Depends(get_db),user:User=Depends(require_roles(*ADMIN_ROLES))):
    return register_or_heartbeat(db,p.node_name,p.node_type,p.address,p.capabilities)

@router.get('/nodes')
def nodes(db:Session=Depends(get_db),user:User=Depends(current_user)):
    cluster_summary(db)
    return db.scalars(select(ClusterNode).order_by(ClusterNode.node_type,ClusterNode.node_name)).all()

@router.post('/leases/acquire')
def lease(p:LeaseIn,db:Session=Depends(get_db),user:User=Depends(require_roles(*ADMIN_ROLES))):
    node=db.get(ClusterNode,p.node_id)
    if not node: raise HTTPException(404,'Nó não encontrado')
    won,obj=acquire_lease(db,p.name,node,p.ttl_seconds)
    return {'acquired':won,'lease':obj}

@router.get('/failovers')
def failovers(db:Session=Depends(get_db),user:User=Depends(require_roles(*ADMIN_ROLES))):
    return db.scalars(select(FailoverEvent).order_by(FailoverEvent.created_at.desc()).limit(300)).all()

@router.post('/backup-policies')
def create_backup_policy(p:BackupPolicyIn,db:Session=Depends(get_db),user:User=Depends(require_roles(Role.admin,Role.director,Role.data_engineer))):
    try:nxt=next_cron(p.schedule_cron)
    except Exception as exc: raise HTTPException(422,f'Cron inválido: {exc}')
    obj=BackupPolicy(**p.model_dump(),next_run_at=nxt,created_by=user.id); db.add(obj); db.commit(); db.refresh(obj); return obj

@router.get('/backup-policies')
def backup_policies(db:Session=Depends(get_db),user:User=Depends(require_roles(*ADMIN_ROLES))):
    return db.scalars(select(BackupPolicy).order_by(BackupPolicy.created_at.desc())).all()

@router.post('/backup-policies/{policy_id}/run')
def run_backup(policy_id:str,db:Session=Depends(get_db),user:User=Depends(require_roles(Role.admin,Role.director,Role.data_engineer))):
    policy=db.get(BackupPolicy,policy_id)
    if not policy: raise HTTPException(404,'Política não encontrada')
    return execute_backup(db,policy,user.id)

@router.get('/backup-runs')
def backup_runs(db:Session=Depends(get_db),user:User=Depends(require_roles(*ADMIN_ROLES))):
    return db.scalars(select(BackupRun).order_by(BackupRun.created_at.desc()).limit(300)).all()

@router.post('/dr-plans')
def create_dr_plan(p:DRPlanIn,db:Session=Depends(get_db),user:User=Depends(require_roles(Role.admin,Role.director,Role.data_engineer))):
    obj=DisasterRecoveryPlan(**p.model_dump(),owner_id=user.id); db.add(obj); db.commit(); db.refresh(obj); return obj

@router.get('/dr-plans')
def dr_plans(db:Session=Depends(get_db),user:User=Depends(require_roles(*ADMIN_ROLES))):
    return db.scalars(select(DisasterRecoveryPlan).order_by(DisasterRecoveryPlan.created_at.desc())).all()

@router.post('/dr-plans/{plan_id}/approve')
def approve_plan(plan_id:str,db:Session=Depends(get_db),user:User=Depends(require_roles(Role.admin,Role.director))):
    plan=db.get(DisasterRecoveryPlan,plan_id)
    if not plan: raise HTTPException(404,'Plano não encontrado')
    plan.status='approved'; plan.approved_by=user.id; plan.approved_at=datetime.now(timezone.utc); db.commit(); db.refresh(plan); return plan

@router.post('/dr-plans/{plan_id}/drills')
def create_drill(plan_id:str,p:DRDrillIn,db:Session=Depends(get_db),user:User=Depends(require_roles(Role.admin,Role.director,Role.data_engineer))):
    if not db.get(DisasterRecoveryPlan,plan_id): raise HTTPException(404,'Plano não encontrado')
    obj=DisasterRecoveryDrill(plan_id=plan_id,scenario=p.scenario,scheduled_at=p.scheduled_at,status='planned'); db.add(obj); db.commit(); db.refresh(obj); return obj

@router.patch('/drills/{drill_id}/finish')
def finish_drill(drill_id:str,p:DRDrillFinish,db:Session=Depends(get_db),user:User=Depends(require_roles(Role.admin,Role.director,Role.data_engineer))):
    drill=db.get(DisasterRecoveryDrill,drill_id)
    if not drill: raise HTTPException(404,'Teste não encontrado')
    plan=db.get(DisasterRecoveryPlan,drill.plan_id)
    drill.status='completed'; drill.executed_by=user.id; drill.finished_at=datetime.now(timezone.utc); drill.measured_rpo_minutes=p.measured_rpo_minutes; drill.measured_rto_minutes=p.measured_rto_minutes; drill.results=p.results; drill.passed=p.measured_rpo_minutes<=plan.rpo_minutes and p.measured_rto_minutes<=plan.rto_minutes
    db.commit(); db.refresh(drill); return drill

@router.get('/drills')
def drills(db:Session=Depends(get_db),user:User=Depends(require_roles(*ADMIN_ROLES))):
    return db.scalars(select(DisasterRecoveryDrill).order_by(DisasterRecoveryDrill.created_at.desc()).limit(300)).all()
