"""Mandate service: step advancement logic, assignment and revocation."""

from __future__ import annotations

import uuid
from datetime import datetime, timedelta, timezone

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.models.mandate import Mandate, MandateStep
from app.models.protocol import VotingProtocol
from app.models.user import DuniterIdentity
from app.models.vote import VoteSession


# Valid status transitions for mandates
_MANDATE_STATUS_ORDER = [
    "draft",
    "candidacy",
    "voting",
    "active",
    "reporting",
    "completed",
]


async def advance_mandate(mandate_id: uuid.UUID, db: AsyncSession) -> Mandate:
    """Move a mandate to its next step.

    Completes the current active step and activates the next pending step.
    If no more steps remain, the mandate status advances to the next phase.

    Parameters
    ----------
    mandate_id:
        UUID of the Mandate to advance.
    db:
        Async database session.

    Returns
    -------
    Mandate
        The updated mandate.

    Raises
    ------
    ValueError
        If the mandate is not found, already completed/revoked, or
        no further advancement is possible.
    """
    result = await db.execute(
        select(Mandate)
        .options(selectinload(Mandate.steps))
        .where(Mandate.id == mandate_id)
    )
    mandate = result.scalar_one_or_none()
    if mandate is None:
        raise ValueError(f"Mandat introuvable : {mandate_id}")

    if mandate.status in ("completed", "revoked"):
        raise ValueError(f"Le mandat est deja en statut terminal : {mandate.status}")

    steps: list[MandateStep] = sorted(mandate.steps, key=lambda s: s.step_order)

    # Find the current active step
    active_step: MandateStep | None = None
    for step in steps:
        if step.status == "active":
            active_step = step
            break

    if active_step is not None:
        # Complete the active step
        active_step.status = "completed"

        # Activate the next pending step
        next_step: MandateStep | None = None
        for step in steps:
            if step.step_order > active_step.step_order and step.status == "pending":
                next_step = step
                break

        if next_step is not None:
            next_step.status = "active"
        else:
            # No more steps: advance mandate status
            _advance_mandate_status(mandate)
    else:
        # No active step: activate the first pending one
        first_pending: MandateStep | None = None
        for step in steps:
            if step.status == "pending":
                first_pending = step
                break

        if first_pending is not None:
            first_pending.status = "active"
            # Move out of draft
            if mandate.status == "draft":
                mandate.status = "candidacy"
        else:
            # All steps completed: advance status
            _advance_mandate_status(mandate)

    await db.commit()
    await db.refresh(mandate)

    return mandate


def _advance_mandate_status(mandate: Mandate) -> None:
    """Move a mandate to its next status in the lifecycle."""
    try:
        current_index = _MANDATE_STATUS_ORDER.index(mandate.status)
    except ValueError:
        return

    next_index = current_index + 1
    if next_index < len(_MANDATE_STATUS_ORDER):
        mandate.status = _MANDATE_STATUS_ORDER[next_index]


async def assign_mandatee(
    mandate_id: uuid.UUID,
    mandatee_id: uuid.UUID,
    db: AsyncSession,
) -> Mandate:
    """Assign a mandatee (DuniterIdentity) to a mandate.

    The mandate must be in a state that accepts assignment (not completed/revoked).
    The mandatee must exist in the duniter_identities table.

    Parameters
    ----------
    mandate_id:
        UUID of the Mandate.
    mandatee_id:
        UUID of the DuniterIdentity to assign.
    db:
        Async database session.

    Returns
    -------
    Mandate
        The updated mandate.

    Raises
    ------
    ValueError
        If the mandate or mandatee is not found, or the mandate
        is in a terminal state.
    """
    result = await db.execute(
        select(Mandate)
        .options(selectinload(Mandate.steps))
        .where(Mandate.id == mandate_id)
    )
    mandate = result.scalar_one_or_none()
    if mandate is None:
        raise ValueError(f"Mandat introuvable : {mandate_id}")

    if mandate.status in ("completed", "revoked"):
        raise ValueError(f"Impossible d'assigner un mandataire : le mandat est en statut terminal ({mandate.status})")

    # Verify mandatee exists
    identity_result = await db.execute(
        select(DuniterIdentity).where(DuniterIdentity.id == mandatee_id)
    )
    identity = identity_result.scalar_one_or_none()
    if identity is None:
        raise ValueError(f"Identite Duniter introuvable : {mandatee_id}")

    mandate.mandatee_id = mandatee_id
    mandate.starts_at = datetime.now(timezone.utc)

    await db.commit()
    await db.refresh(mandate)

    return mandate


async def revoke_mandate(
    mandate_id: uuid.UUID,
    db: AsyncSession,
) -> Mandate:
    """Revoke an active mandate.

    Sets the mandate status to 'revoked' and cancels any active/pending steps.

    Parameters
    ----------
    mandate_id:
        UUID of the Mandate to revoke.
    db:
        Async database session.

    Returns
    -------
    Mandate
        The updated mandate.

    Raises
    ------
    ValueError
        If the mandate is not found or already in a terminal state.
    """
    result = await db.execute(
        select(Mandate)
        .options(selectinload(Mandate.steps))
        .where(Mandate.id == mandate_id)
    )
    mandate = result.scalar_one_or_none()
    if mandate is None:
        raise ValueError(f"Mandat introuvable : {mandate_id}")

    if mandate.status in ("completed", "revoked"):
        raise ValueError(f"Le mandat est deja en statut terminal : {mandate.status}")

    # Cancel active and pending steps
    for step in mandate.steps:
        if step.status in ("active", "pending"):
            step.status = "cancelled"

    mandate.status = "revoked"
    mandate.ends_at = datetime.now(timezone.utc)

    await db.commit()
    await db.refresh(mandate)

    return mandate


async def create_vote_session_for_step(
    mandate_id: uuid.UUID,
    step_id: uuid.UUID,
    db: AsyncSession,
) -> VoteSession:
    """Create a VoteSession linked to a mandate step.

    Uses the mandate's linked decision's voting_protocol to configure
    the session, or requires a voting_protocol_id on the mandate's decision.

    Parameters
    ----------
    mandate_id:
        UUID of the Mandate.
    step_id:
        UUID of the MandateStep to attach the vote session to.
    db:
        Async database session.

    Returns
    -------
    VoteSession
        The newly created vote session.

    Raises
    ------
    ValueError
        If the mandate, step, or protocol is not found, or the step
        already has a vote session.
    """
    # Fetch mandate with steps
    result = await db.execute(
        select(Mandate)
        .options(selectinload(Mandate.steps))
        .where(Mandate.id == mandate_id)
    )
    mandate = result.scalar_one_or_none()
    if mandate is None:
        raise ValueError(f"Mandat introuvable : {mandate_id}")

    # Find the step
    step: MandateStep | None = None
    for s in mandate.steps:
        if s.id == step_id:
            step = s
            break

    if step is None:
        raise ValueError(f"Etape introuvable : {step_id}")

    # Check step doesn't already have a vote session
    if step.vote_session_id is not None:
        raise ValueError("Cette etape possede deja une session de vote")

    # Resolve the voting protocol via the linked decision
    if mandate.decision_id is None:
        raise ValueError("Aucune decision liee a ce mandat pour determiner le protocole de vote")

    from app.models.decision import Decision

    decision_result = await db.execute(
        select(Decision).where(Decision.id == mandate.decision_id)
    )
    decision = decision_result.scalar_one_or_none()
    if decision is None or decision.voting_protocol_id is None:
        raise ValueError("Aucun protocole de vote configure pour la decision liee")

    # Fetch the voting protocol and its formula config for duration
    proto_result = await db.execute(
        select(VotingProtocol)
        .options(selectinload(VotingProtocol.formula_config))
        .where(VotingProtocol.id == decision.voting_protocol_id)
    )
    protocol = proto_result.scalar_one_or_none()
    if protocol is None:
        raise ValueError("Protocole de vote introuvable")

    duration_days = protocol.formula_config.duration_days if protocol.formula_config else 30
    now = datetime.now(timezone.utc)

    # Create the vote session with explicit UUID (needed before flush)
    session_id = uuid.uuid4()
    session = VoteSession(
        id=session_id,
        decision_id=mandate.decision_id,
        voting_protocol_id=protocol.id,
        starts_at=now,
        ends_at=now + timedelta(days=duration_days),
        status="open",
    )
    db.add(session)

    # Link session to step
    step.vote_session_id = session_id

    await db.commit()
    await db.refresh(session)

    return session
