import json
from collections import Counter
from typing import Any, Dict, List, Optional, Tuple

from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

from app.models.entities import Poll, PollStatus, Room, Vote
from app.services.time_utils import as_utc_naive, utc_now


def _parse_json(text: str, fallback: Any) -> Any:
    try:
        value = json.loads(text or "")
        return value if value is not None else fallback
    except json.JSONDecodeError:
        return fallback


def parse_options(poll: Poll) -> List[str]:
    data = _parse_json(poll.options_json, [])
    if isinstance(data, list):
        return [str(x) for x in data]
    return []


def parse_poll_config(poll: Poll) -> Dict[str, Any]:
    data = _parse_json(poll.config_json, {})
    if not isinstance(data, dict):
        return {}
    data.setdefault("trigger_keys", [])
    data.setdefault("trigger_names", [])
    data.setdefault("pause_action", {})
    data.setdefault("winner_actions", {})
    data.setdefault("duration_seconds", 0)
    return data


def parse_poll_runtime(poll: Poll) -> Dict[str, Any]:
    data = _parse_json(poll.runtime_json, {})
    return data if isinstance(data, dict) else {}


def set_poll_config(poll: Poll, data: Dict[str, Any]) -> None:
    poll.config_json = json.dumps(data, ensure_ascii=False)


def set_poll_runtime(poll: Poll, data: Dict[str, Any]) -> None:
    poll.runtime_json = json.dumps(data, ensure_ascii=False)


def next_command_seq(poll: Poll) -> int:
    runtime = parse_poll_runtime(poll)
    current = int(((runtime.get("controller_command") or {}).get("seq") or 0))
    return current + 1


def build_winner_command(poll: Poll, winner_option: Optional[str], source: str) -> Optional[Dict[str, Any]]:
    if not winner_option:
        return None
    config = parse_poll_config(poll)
    winner_actions = config.get("winner_actions") or {}
    action = winner_actions.get(winner_option)
    if not isinstance(action, dict) or not action:
        return None
    return {
        "seq": next_command_seq(poll),
        "winner_option": winner_option,
        "source": source,
        "action": action,
        "issued_at": utc_now().isoformat() + "Z",
    }


def build_open_pause_command(poll: Poll, source: str) -> Optional[Dict[str, Any]]:
    config = parse_poll_config(poll)
    action = config.get("pause_action")
    if not isinstance(action, dict) or not action:
        return None
    return {
        "seq": next_command_seq(poll),
        "winner_option": None,
        "source": source,
        "action": action,
        "issued_at": utc_now().isoformat() + "Z",
    }


def apply_control_command(poll: Poll, command: Optional[Dict[str, Any]]) -> None:
    runtime = parse_poll_runtime(poll)
    if command:
        runtime["controller_command"] = command
    set_poll_runtime(poll, runtime)


async def ensure_room(session: AsyncSession, room_id: str, name: str = "") -> Room:
    r = await session.get(Room, room_id)
    if r:
        return r
    r = Room(id=room_id, name=name)
    session.add(r)
    await session.flush()
    return r


async def compute_tally(session: AsyncSession, poll_id: str) -> Dict[str, int]:
    result = await session.exec(select(Vote).where(Vote.poll_id == poll_id))
    votes = result.all()
    choices = [v.choice for v in votes]
    counts = Counter(choices)
    return dict(counts)


def pick_winner(option_counts: Dict[str, int], options: List[str]) -> Tuple[Optional[str], bool]:
    """Returns (winner_choice_or_None, is_tie)."""
    if not option_counts:
        return None, False
    best = max(option_counts.values())
    if best <= 0:
        return None, False
    winners = [k for k, v in option_counts.items() if v == best]
    if len(winners) > 1:
        ordered = [o for o in options if o in winners]
        if ordered:
            return ordered[0], True
        return winners[0], True
    return winners[0], False


async def close_poll_and_set_winner(session: AsyncSession, poll: Poll) -> Poll:
    poll.status = PollStatus.closed
    options = parse_options(poll)
    tally = await compute_tally(session, poll.id)
    for opt in options:
        tally.setdefault(opt, 0)
    winner, _ = pick_winner(tally, options)
    poll.winner_option = winner
    poll.winner_source = "vote_result"
    runtime = parse_poll_runtime(poll)
    runtime["close_reason"] = runtime.get("close_reason") or "vote_result"
    runtime["closed_at"] = utc_now().isoformat() + "Z"
    set_poll_runtime(poll, runtime)
    apply_control_command(poll, build_winner_command(poll, winner, "vote_result"))
    session.add(poll)
    await session.flush()
    return poll


async def cast_vote(
    session: AsyncSession,
    poll: Poll,
    voter_id: str,
    choice: str,
) -> Vote:
    options = parse_options(poll)
    if choice not in options:
        raise ValueError("invalid_choice")
    if not can_accept_vote(poll):
        raise ValueError("poll_not_accepting")

    result = await session.exec(
        select(Vote).where(Vote.poll_id == poll.id, Vote.voter_id == voter_id)
    )
    existing = result.first()
    if existing:
        existing.choice = choice
        existing.created_at = utc_now()
        session.add(existing)
        await session.flush()
        return existing
    v = Vote(poll_id=poll.id, voter_id=voter_id, choice=choice)
    session.add(v)
    await session.flush()
    return v


def can_accept_vote(poll: Poll) -> bool:
    if poll.status != PollStatus.open:
        return False
    deadline_at = as_utc_naive(poll.deadline_at)
    if deadline_at and utc_now() > deadline_at:
        return False
    return True
