"""Protocols router: voting protocols, formula configurations, and simulation.""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.database import get_db from app.engine.smith_threshold import smith_threshold as compute_smith_threshold from app.engine.techcomm_threshold import techcomm_threshold as compute_techcomm_threshold from app.engine.threshold import wot_threshold as compute_wot_threshold from app.models.protocol import FormulaConfig, VotingProtocol from app.models.user import DuniterIdentity from app.schemas.protocol import ( FormulaConfigCreate, FormulaConfigOut, FormulaConfigUpdate, FormulaSimulationRequest, FormulaSimulationResult, VotingProtocolCreate, VotingProtocolOut, VotingProtocolUpdate, ) from app.services.auth_service import get_current_identity router = APIRouter() # ── Helpers ───────────────────────────────────────────────────────────────── async def _get_protocol(db: AsyncSession, protocol_id: uuid.UUID) -> VotingProtocol: """Fetch a voting protocol by ID with its formula config, or raise 404.""" result = await db.execute( select(VotingProtocol) .options(selectinload(VotingProtocol.formula_config)) .where(VotingProtocol.id == protocol_id) ) protocol = result.scalar_one_or_none() if protocol is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Protocole introuvable") return protocol async def _get_formula(db: AsyncSession, formula_id: uuid.UUID) -> FormulaConfig: """Fetch a formula config by ID, or raise 404.""" result = await db.execute( select(FormulaConfig).where(FormulaConfig.id == formula_id) ) formula = result.scalar_one_or_none() if formula is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Configuration de formule introuvable") return formula # ── Voting Protocol routes ────────────────────────────────────────────────── @router.get("/", response_model=list[VotingProtocolOut]) async def list_protocols( db: AsyncSession = Depends(get_db), vote_type: str | None = Query(default=None, description="Filtrer par type de vote (binary, nuanced)"), skip: int = Query(default=0, ge=0), limit: int = Query(default=50, ge=1, le=200), ) -> list[VotingProtocolOut]: """List all voting protocols with their formula configurations.""" stmt = select(VotingProtocol).options(selectinload(VotingProtocol.formula_config)) if vote_type is not None: stmt = stmt.where(VotingProtocol.vote_type == vote_type) stmt = stmt.order_by(VotingProtocol.created_at.desc()).offset(skip).limit(limit) result = await db.execute(stmt) protocols = result.scalars().unique().all() return [VotingProtocolOut.model_validate(p) for p in protocols] @router.post("/", response_model=VotingProtocolOut, status_code=status.HTTP_201_CREATED) async def create_protocol( payload: VotingProtocolCreate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> VotingProtocolOut: """Create a new voting protocol. The formula_config_id must reference an existing FormulaConfig. """ # Verify formula config exists fc_result = await db.execute( select(FormulaConfig).where(FormulaConfig.id == payload.formula_config_id) ) if fc_result.scalar_one_or_none() is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Configuration de formule introuvable", ) protocol = VotingProtocol(**payload.model_dump()) db.add(protocol) await db.commit() await db.refresh(protocol) # Reload with formula config protocol = await _get_protocol(db, protocol.id) return VotingProtocolOut.model_validate(protocol) @router.get("/{id}", response_model=VotingProtocolOut) async def get_protocol( id: uuid.UUID, db: AsyncSession = Depends(get_db), ) -> VotingProtocolOut: """Get a single voting protocol with its formula configuration.""" protocol = await _get_protocol(db, id) return VotingProtocolOut.model_validate(protocol) @router.put("/{id}", response_model=VotingProtocolOut) async def update_protocol( id: uuid.UUID, payload: VotingProtocolUpdate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> VotingProtocolOut: """Update a voting protocol (meta-governance). Only provided fields will be updated. Requires authentication. """ protocol = await _get_protocol(db, id) update_data = payload.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(protocol, field, value) await db.commit() # Reload with formula config protocol = await _get_protocol(db, protocol.id) return VotingProtocolOut.model_validate(protocol) # ── Formula Config routes ─────────────────────────────────────────────────── @router.get("/formulas", response_model=list[FormulaConfigOut]) async def list_formulas( db: AsyncSession = Depends(get_db), skip: int = Query(default=0, ge=0), limit: int = Query(default=50, ge=1, le=200), ) -> list[FormulaConfigOut]: """List all formula configurations.""" stmt = ( select(FormulaConfig) .order_by(FormulaConfig.created_at.desc()) .offset(skip) .limit(limit) ) result = await db.execute(stmt) formulas = result.scalars().all() return [FormulaConfigOut.model_validate(f) for f in formulas] @router.post("/formulas", response_model=FormulaConfigOut, status_code=status.HTTP_201_CREATED) async def create_formula( payload: FormulaConfigCreate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> FormulaConfigOut: """Create a new formula configuration for WoT threshold computation.""" formula = FormulaConfig(**payload.model_dump()) db.add(formula) await db.commit() await db.refresh(formula) return FormulaConfigOut.model_validate(formula) @router.get("/formulas/{id}", response_model=FormulaConfigOut) async def get_formula( id: uuid.UUID, db: AsyncSession = Depends(get_db), ) -> FormulaConfigOut: """Get a single formula configuration.""" formula = await _get_formula(db, id) return FormulaConfigOut.model_validate(formula) @router.put("/formulas/{id}", response_model=FormulaConfigOut) async def update_formula( id: uuid.UUID, payload: FormulaConfigUpdate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> FormulaConfigOut: """Update a formula configuration (meta-governance). Only provided fields will be updated. Requires authentication. """ formula = await _get_formula(db, id) update_data = payload.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(formula, field, value) await db.commit() await db.refresh(formula) return FormulaConfigOut.model_validate(formula) # ── Simulation ────────────────────────────────────────────────────────────── @router.post("/simulate", response_model=FormulaSimulationResult) async def simulate_formula( payload: FormulaSimulationRequest, ) -> FormulaSimulationResult: """Simulate a WoT threshold formula computation. Pure calculation endpoint -- no database access needed. Accepts WoT size, total votes, and formula parameters, returns the computed thresholds and derived values. """ # Compute WoT threshold wot_thresh = compute_wot_threshold( wot_size=payload.wot_size, total_votes=payload.total_votes, majority_pct=payload.majority_pct, base_exponent=payload.base_exponent, gradient_exponent=payload.gradient_exponent, constant_base=payload.constant_base, ) # Compute derived values for transparency w = payload.wot_size t = payload.total_votes m = payload.majority_pct / 100.0 g = payload.gradient_exponent participation_rate = t / w if w > 0 else 0.0 turnout_ratio = min(t / w, 1.0) if w > 0 else 0.0 inertia_factor = 1.0 - turnout_ratio ** g if t > 0 else 1.0 required_ratio = m + (1.0 - m) * inertia_factor # Smith threshold (optional) smith_thresh = None if payload.smith_wot_size is not None and payload.smith_exponent is not None: smith_thresh = compute_smith_threshold(payload.smith_wot_size, payload.smith_exponent) # TechComm threshold (optional) techcomm_thresh = None if payload.techcomm_size is not None and payload.techcomm_exponent is not None: techcomm_thresh = compute_techcomm_threshold(payload.techcomm_size, payload.techcomm_exponent) return FormulaSimulationResult( wot_threshold=wot_thresh, smith_threshold=smith_thresh, techcomm_threshold=techcomm_thresh, participation_rate=round(participation_rate, 6), required_ratio=round(required_ratio, 6), inertia_factor=round(inertia_factor, 6), )