"""
Phase 74: Multi-Agent Consensus Voting

N agents must agree before major changes are applied.  Supports quorum
thresholds, timeouts, cancellation, and full voting history.

Storage:
  state/broker/votes/{vote_id}.json
  state/broker/votes/history.jsonl
"""

import json
import uuid
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional

from .server import BROKER_DIR, _write_json_sync, _read_jsonl_sync, _append_jsonl_sync

VOTES_DIR = BROKER_DIR / "votes"


def _ensure_dir() -> None:
    VOTES_DIR.mkdir(parents=True, exist_ok=True)


def _vote_path(vote_id: str) -> Path:
    return VOTES_DIR / f"{vote_id}.json"


def _history_path() -> Path:
    return VOTES_DIR / "history.jsonl"


def _load(vote_id: str) -> Optional[dict]:
    path = _vote_path(vote_id)
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return None


def _save(vote: dict) -> None:
    _ensure_dir()
    _write_json_sync(_vote_path(vote["vote_id"]), vote)


def _log_event(event: dict) -> None:
    _ensure_dir()
    _append_jsonl_sync(_history_path(), event)


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


# ── Public API ────────────────────────────────────────────────────────────


def create_vote(
    topic: str,
    description: str,
    options: list[str],
    required_approvals: int = 3,
    timeout_minutes: int = 30,
    initiator_id: Optional[str] = None,
) -> dict:
    """Start a new consensus vote.

    *options* is a list of choices (e.g. ``["approve", "reject"]``).
    The vote passes when a single option reaches *required_approvals* votes.
    """
    _ensure_dir()
    if len(options) < 2:
        return {"error": "At least 2 options required"}

    vote_id = uuid.uuid4().hex[:12]
    now = _now_iso()
    expires_at = (
        datetime.now(timezone.utc) + timedelta(minutes=timeout_minutes)
    ).isoformat()

    vote = {
        "vote_id": vote_id,
        "topic": topic,
        "description": description,
        "options": options,
        "required_approvals": required_approvals,
        "timeout_minutes": timeout_minutes,
        "initiator_id": initiator_id or "",
        "status": "open",
        "votes": {},          # agent_id -> {"choice": ..., "reason": ..., "timestamp": ...}
        "tally": {opt: 0 for opt in options},
        "created_at": now,
        "expires_at": expires_at,
        "resolved_at": None,
        "result": None,       # approved / rejected / tie / expired
        "winning_option": None,
        "cancelled_at": None,
        "cancel_reason": None,
    }
    _save(vote)
    _log_event({"event": "created", "vote_id": vote_id, "topic": topic, "timestamp": now})
    return vote


def cast_vote(
    vote_id: str,
    agent_id: str,
    choice: str,
    reason: str = "",
) -> dict:
    """Cast or change a vote.  One vote per agent."""
    vote = _load(vote_id)
    if vote is None:
        return {"error": "Vote not found"}
    if vote["status"] != "open":
        return {"error": f"Vote is {vote['status']}, not open"}
    if choice not in vote["options"]:
        return {"error": f"Invalid choice. Options: {vote['options']}"}

    # Check timeout
    now_dt = datetime.now(timezone.utc)
    expires = datetime.fromisoformat(vote["expires_at"])
    if now_dt >= expires:
        vote["status"] = "expired"
        vote["result"] = "expired"
        vote["resolved_at"] = _now_iso()
        _save(vote)
        return {"error": "Vote has expired"}

    now = _now_iso()

    # Remove previous vote from tally
    prev = vote["votes"].get(agent_id)
    if prev is not None:
        old_choice = prev["choice"]
        vote["tally"][old_choice] = max(0, vote["tally"].get(old_choice, 1) - 1)

    vote["votes"][agent_id] = {
        "choice": choice,
        "reason": reason,
        "timestamp": now,
    }
    vote["tally"][choice] = vote["tally"].get(choice, 0) + 1
    _save(vote)

    _log_event({
        "event": "vote_cast",
        "vote_id": vote_id,
        "agent_id": agent_id,
        "choice": choice,
        "timestamp": now,
    })
    return {"agent_id": agent_id, "choice": choice, "tally": vote["tally"]}


def get_vote_status(vote_id: str) -> dict:
    """Current tally, who voted what, and time remaining."""
    vote = _load(vote_id)
    if vote is None:
        return {"error": "Vote not found"}

    now_dt = datetime.now(timezone.utc)
    expires = datetime.fromisoformat(vote["expires_at"])
    remaining_sec = max(0, (expires - now_dt).total_seconds())

    return {
        "vote_id": vote_id,
        "topic": vote["topic"],
        "status": vote["status"],
        "options": vote["options"],
        "tally": vote["tally"],
        "votes": vote["votes"],
        "required_approvals": vote["required_approvals"],
        "time_remaining_seconds": round(remaining_sec, 1),
        "result": vote.get("result"),
    }


def check_result(vote_id: str) -> dict:
    """Evaluate whether quorum is met or timeout reached.

    Returns ``result``: ``approved``, ``rejected``, ``tie``, ``expired``,
    or ``pending``.
    """
    vote = _load(vote_id)
    if vote is None:
        return {"error": "Vote not found"}

    # Already resolved?
    if vote["status"] in ("resolved", "cancelled", "expired"):
        return {
            "vote_id": vote_id,
            "result": vote.get("result", vote["status"]),
            "winning_option": vote.get("winning_option"),
        }

    now_dt = datetime.now(timezone.utc)
    expires = datetime.fromisoformat(vote["expires_at"])

    # Check quorum
    required = vote["required_approvals"]
    tally = vote["tally"]
    winners = [opt for opt, count in tally.items() if count >= required]

    if len(winners) == 1:
        vote["status"] = "resolved"
        vote["result"] = "approved"
        vote["winning_option"] = winners[0]
        vote["resolved_at"] = _now_iso()
        _save(vote)
        _log_event({
            "event": "resolved",
            "vote_id": vote_id,
            "result": "approved",
            "winning_option": winners[0],
            "timestamp": vote["resolved_at"],
        })
        return {"vote_id": vote_id, "result": "approved", "winning_option": winners[0]}

    if len(winners) > 1:
        vote["status"] = "resolved"
        vote["result"] = "tie"
        vote["resolved_at"] = _now_iso()
        _save(vote)
        _log_event({
            "event": "resolved",
            "vote_id": vote_id,
            "result": "tie",
            "timestamp": vote["resolved_at"],
        })
        return {"vote_id": vote_id, "result": "tie", "tied_options": winners}

    # Check timeout
    if now_dt >= expires:
        # Determine winner by highest tally, or expired
        best_count = max(tally.values()) if tally else 0
        if best_count > 0:
            top = [opt for opt, c in tally.items() if c == best_count]
            if len(top) == 1:
                vote["status"] = "resolved"
                vote["result"] = "approved"
                vote["winning_option"] = top[0]
            else:
                vote["status"] = "resolved"
                vote["result"] = "tie"
        else:
            vote["status"] = "expired"
            vote["result"] = "expired"
        vote["resolved_at"] = _now_iso()
        _save(vote)
        _log_event({
            "event": "timeout",
            "vote_id": vote_id,
            "result": vote["result"],
            "timestamp": vote["resolved_at"],
        })
        return {
            "vote_id": vote_id,
            "result": vote["result"],
            "winning_option": vote.get("winning_option"),
        }

    return {"vote_id": vote_id, "result": "pending"}


def list_votes(status: Optional[str] = None) -> list[dict]:
    """List all votes, optionally filtered by status."""
    _ensure_dir()
    results: list[dict] = []
    for f in VOTES_DIR.glob("*.json"):
        v = _load(f.stem)
        if v and (status is None or v.get("status") == status):
            results.append(v)
    results.sort(key=lambda v: v.get("created_at", ""), reverse=True)
    return results


def cancel_vote(vote_id: str, reason: str = "") -> dict:
    """Cancel an active vote."""
    vote = _load(vote_id)
    if vote is None:
        return {"error": "Vote not found"}
    if vote["status"] != "open":
        return {"error": f"Vote is {vote['status']}, cannot cancel"}

    now = _now_iso()
    vote["status"] = "cancelled"
    vote["cancelled_at"] = now
    vote["cancel_reason"] = reason
    vote["result"] = "cancelled"
    _save(vote)
    _log_event({
        "event": "cancelled",
        "vote_id": vote_id,
        "reason": reason,
        "timestamp": now,
    })
    return vote


def get_voting_history(agent_id: Optional[str] = None) -> list[dict]:
    """Return past vote events from the history log.

    If *agent_id* is given, filter to events involving that agent.
    """
    _ensure_dir()
    events = _read_jsonl_sync(_history_path())
    if agent_id:
        events = [e for e in events if e.get("agent_id") == agent_id]
    return events
