from __future__ import annotations

from typing import Optional, Tuple, Callable, Dict, Any, List
from datetime import datetime, timedelta, timezone

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

from src.database.models import (
    User, Strategy, StrategyDayState,
)
from src.database.enums import StrategyStatus, DayStatus, FailReason


# ───────────────────────── utilities ─────────────────────────

def _max_day(steps: Dict[str, Any]) -> int:
    if not steps:
        return 0
    keys = []
    for k in steps.keys():
        try:
            keys.append(int(k))
        except Exception:
            continue
    return max(keys) if keys else 0

async def _get_strategy(session: AsyncSession, strategy_id: int) -> Optional[Strategy]:
    return await session.get(Strategy, strategy_id)

async def _get_active_strategy(session: AsyncSession, user_id: int) -> Optional[Strategy]:
    res = await session.execute(
        select(Strategy)
        .where(Strategy.user_id == user_id, Strategy.status == StrategyStatus.active)
        .order_by(Strategy.id.desc())      # берем самую свежую
        .limit(1)
    )
    return res.scalar_one_or_none()

async def _get_day_state(session: AsyncSession, strategy_id: int, day_number: int) -> Optional[StrategyDayState]:
    res = await session.execute(
        select(StrategyDayState).where(
            StrategyDayState.strategy_id == strategy_id,
            StrategyDayState.day_number == day_number,
        )
    )
    return res.scalar_one_or_none()

async def _ensure_day_state(session: AsyncSession, strategy_id: int, day_number: int) -> StrategyDayState:
    row = await _get_day_state(session, strategy_id, day_number)
    if row:
        return row
    row = StrategyDayState(
        strategy_id=strategy_id,
        day_number=day_number,
        status=DayStatus.planned,
        fail_reason=None,
        snooze_until=None,
    )
    session.add(row)
    await session.flush()
    return row


# ───────────────────────── create / read ─────────────────────────

async def create_strategy(
    session: AsyncSession,
    *,
    user_id: int,
    strategy_text: str,
    steps: Dict[str, Any],
    make_active: bool = True,
    raise_if_active_exists: bool = True,
) -> Strategy:
    if make_active:
        existing = await _get_active_strategy(session, user_id)
        if existing and raise_if_active_exists:
            raise ValueError("активная стратегия уже существует")

    strat = Strategy(
        user_id=user_id,
        strategy=strategy_text,
        steps=steps,
        status=StrategyStatus.active if make_active else StrategyStatus.completed,
        current_day=1,
    )
    session.add(strat)
    await session.flush()

    if _max_day(steps) >= 1:
        await _ensure_day_state(session, strat.id, 1)
    return strat

async def get_active_strategy(session: AsyncSession, user_id: int) -> Optional[Strategy]:
    return await _get_active_strategy(session, user_id)

async def get_strategy_by_id(session: AsyncSession, strategy_id: int) -> Optional[Strategy]:
    return await _get_strategy(session, strategy_id)


# ───────────────────────── issuing / progress ─────────────────────────

async def issue_today(session: AsyncSession, strategy_id: int) -> StrategyDayState:
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")
    day = strat.current_day
    state = await _ensure_day_state(session, strategy_id, day)
    if state.status in (DayStatus.planned, DayStatus.snoozed, DayStatus.hint, DayStatus.failed):
        state.status = DayStatus.issued
    await session.flush()
    return state

async def complete_today(session: AsyncSession, strategy_id: int) -> Tuple[Strategy, StrategyDayState, int]:
    """
    Mark today's day as done and advance current_day.
    Возвращает (стратегия, state, total_done).
    """
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")

    day = strat.current_day
    state = await _ensure_day_state(session, strategy_id, day)

    state.status = DayStatus.done
    state.fail_reason = None
    state.snooze_until = None
    await session.flush()

    # advance cursor
    next_day = day + 1
    maxday = _max_day(strat.steps or {})
    if next_day > maxday or maxday == 0:
        strat.status = StrategyStatus.completed
    else:
        strat.current_day = next_day
        await _ensure_day_state(session, strat.id, next_day)

    await session.flush()

    # считаем количество выполненных
    res = await session.execute(
        select(StrategyDayState).where(
            StrategyDayState.strategy_id == strat.id,
            StrategyDayState.status == DayStatus.done
        )
    )
    total_done = len(res.scalars().all())

    return strat, state, total_done


async def snooze_today(session: AsyncSession, strategy_id: int, hours: int = 24) -> StrategyDayState:
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")
    state = await _ensure_day_state(session, strategy_id, strat.current_day)
    state.status = DayStatus.snoozed
    state.snooze_until = datetime.now(timezone.utc) + timedelta(hours=hours)
    await session.flush()
    return state

async def ask_hint_today(session: AsyncSession, strategy_id: int) -> StrategyDayState:
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")
    state = await _ensure_day_state(session, strategy_id, strat.current_day)
    state.status = DayStatus.hint
    await session.flush()
    return state

async def fail_today(session: AsyncSession, strategy_id: int, reason: FailReason | str = FailReason.other) -> StrategyDayState:
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")
    state = await _ensure_day_state(session, strategy_id, strat.current_day)

    if isinstance(reason, str):
        try:
            reason = FailReason(reason)
        except Exception:
            reason = FailReason.other

    state.status = DayStatus.failed
    state.fail_reason = reason
    state.snooze_until = None
    await session.flush()
    return state


# ───────────────────────── tail ops ─────────────────────────

async def replace_steps_tail(
    session: AsyncSession,
    strategy_id: int,
    from_day_exclusive: int,
    new_tail_steps: Dict[str, Any],
) -> Strategy:
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")

    merged: Dict[str, Any] = {}
    for k, v in (strat.steps or {}).items():
        try:
            day_num = int(k)
        except Exception:
            merged[k] = v
            continue
        if day_num <= from_day_exclusive:
            merged[str(day_num)] = v

    for k, v in (new_tail_steps or {}).items():
        try:
            day_num = int(k)
        except Exception:
            continue
        if day_num > from_day_exclusive:
            merged[str(day_num)] = v

    strat.steps = merged
    await session.flush()
    return strat

async def regenerate_tail_with(
    session: AsyncSession,
    strategy_id: int,
    generator: Callable[[Strategy, int], Dict[str, Any]],
) -> Strategy:
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")

    from_day = strat.current_day
    tail = generator(strat, from_day)
    if not isinstance(tail, dict):
        raise ValueError("генератор вернул некорректные данные (ожидался dict)")
    await replace_steps_tail(session, strategy_id, from_day_exclusive=from_day, new_tail_steps=tail)
    return strat


# ───────────────────────── convenience ─────────────────────────

async def ensure_today_issued(session: AsyncSession, strategy_id: int) -> StrategyDayState:
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")
    state = await _ensure_day_state(session, strategy_id, strat.current_day)
    if state.status in (DayStatus.planned, DayStatus.snoozed, DayStatus.hint, DayStatus.failed):
        state.status = DayStatus.issued
        await session.flush()
    return state

async def get_today_state(session: AsyncSession, strategy_id: int) -> StrategyDayState:
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")
    return await _ensure_day_state(session, strategy_id, strat.current_day)

async def has_more_days(session: AsyncSession, strategy_id: int) -> bool:
    strat = await _get_strategy(session, strategy_id)
    if not strat:
        raise ValueError("стратегия не найдена")
    return strat.current_day < _max_day(strat.steps or {})

async def ensure_single_active_strategy(session: AsyncSession, user_id: int) -> Optional[Strategy]:
    res = await session.execute(
        select(Strategy)
        .where(Strategy.user_id == user_id, Strategy.status == StrategyStatus.active)
        .order_by(Strategy.id.desc())
    )
    actives = list(res.scalars().all())
    if not actives:
        return None
    keeper = actives[0]
    for s in actives[1:]:
        s.status = StrategyStatus.completed
    await session.flush()
    return keeper

async def get_active_strategy_with_steps_by_tg(
    session: AsyncSession,
    tg_id: str,
) -> Tuple[Optional[Strategy], Dict[str, Any], int]:
    user = await session.scalar(select(User).where(User.tg_id == tg_id))
    if not user:
        return None, {}, 0

    # гарантируем единственность активной
    strat = await ensure_single_active_strategy(session, user.id)
    if not strat:
        return None, {}, 0

    steps = strat.steps or {}
    day = strat.current_day or 1
    return strat, steps, day



# ───────────────────────── scheduler helpers ─────────────────────────

async def get_due_for_daily_issue(session: AsyncSession) -> List[Strategy]:
    """
    Кому слать сегодня:
    - стратегия активна;
    - текущий день ещё НЕ done;
    - и (статус PLANNED/FAILED/HINT) ИЛИ (SNOOZED, но snooze_until <= now);
    - и ещё не переведён в ISSUED сегодня (мы это сделаем перед отправкой).
    """
    now = datetime.now(timezone.utc)
    res = await session.execute(
        select(Strategy).where(Strategy.status == StrategyStatus.active)
    )
    strategies = list(res.scalars().all())

    due: List[Strategy] = []
    for strat in strategies:
        state = await _ensure_day_state(session, strat.id, strat.current_day)

        if state.status == DayStatus.done:
            continue

        if state.status == DayStatus.snoozed and state.snooze_until:
            if state.snooze_until > now:
                continue  # ещё рано

        # planned / failed / hint / snoozed(просрочен) — слать
        due.append(strat)

    return due
