import asyncio
import contextlib
import logging
import os
from typing import Optional, List, Dict, Any

from aiogram import Router, F
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.enums import ChatAction
from aiogram.fsm.context import FSMContext
from dotenv import load_dotenv

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

from apscheduler.schedulers.asyncio import AsyncIOScheduler
import pytz

from src.bot.keyboards.daily import kb_daily_step, kb_next_step, kb_fail_reason
from src.bot.utils.badges import get_badge_message
from src.database.session import SessionLocal
from src.database.db_methods.strategy import (
    get_active_strategy_with_steps_by_tg,
    complete_today,
    snooze_today,
    ask_hint_today,
    fail_today,
    ensure_today_issued,
    get_due_for_daily_issue,
    get_today_state,
    _get_day_state,
)
from src.database.enums import DayStatus, FailReason
from src.database.models import User, Strategy
from src.llm.openai_client import ask_json

load_dotenv()

minutes = int(os.getenv("MINUTES"))
router = Router(name="daily_router")
logger = logging.getLogger(__name__)

_scheduler: Optional[AsyncIOScheduler] = None
TZ = pytz.timezone("Europe/Moscow")
JOB_ID = "daily_send_job"

SIMPLIFIED_MARKER = ":::SIMPLIFIED:::"


async def _typing(bot, chat_id: int, seconds: int = 2):
    await bot.send_chat_action(chat_id, ChatAction.TYPING)
    await asyncio.sleep(seconds)


def _is_simplified_step(step_text: str) -> bool:
    return SIMPLIFIED_MARKER in step_text


def _add_simplified_marker(text: str) -> str:
    return f"{text}{SIMPLIFIED_MARKER}"


def _remove_simplified_marker(text: str) -> str:
    return text.replace(SIMPLIFIED_MARKER, "")


@router.callback_query(F.data.startswith("step:done"))
async def step_done(cb: CallbackQuery, state: FSMContext):
    with contextlib.suppress(Exception):
        await cb.message.edit_reply_markup(reply_markup=None)

    tg_id = str(cb.from_user.id)
    await cb.answer()
    await _typing(cb.message.bot, cb.message.chat.id)

    day_from_callback = None
    if ":" in cb.data:
        try:
            day_from_callback = int(cb.data.split(":")[-1])
        except:
            pass

    async with SessionLocal() as session:
        strat, steps, current_day = await get_active_strategy_with_steps_by_tg(session, tg_id)

        if not strat:
            await cb.message.answer("Активная стратегия не найдена. Нажми /start.")
            return

        day_to_complete = day_from_callback if day_from_callback is not None else current_day

        day_state = await _get_day_state(session, strat.id, day_to_complete)
        if day_state and day_state.status == DayStatus.done:
            await cb.message.answer("✅ Это задание уже выполнено!")
            return

        if day_to_complete < current_day:
            await cb.message.answer("✅ Это задание уже выполнено ранее!")
            return

        step_text = steps.get(str(day_to_complete), "")
        is_simplified = _is_simplified_step(step_text)

        total_done = None
        should_count_badge = False

        if is_simplified:
            day_state = await _get_day_state(session, strat.id, day_to_complete)
            if day_state:
                day_state.status = DayStatus.done

            next_step_text = steps.get(str(day_to_complete + 1), "")
            next_is_simplified = _is_simplified_step(next_step_text)

            if next_is_simplified:
                if day_to_complete == current_day:
                    strat.current_day += 1
            else:
                strat, state_row, total_done = await complete_today(session, strat.id)
                should_count_badge = True

            await session.commit()
        else:
            strat, state_row, total_done = await complete_today(session, strat.id)
            should_count_badge = True
            await session.commit()

    await cb.message.answer(
        "Отлично! Задача отмечена выполненной. Завтра пришлю новый шаг",
        reply_markup=kb_next_step()
    )

    if should_count_badge and total_done is not None:
        badge_msg = get_badge_message(total_done)
        if badge_msg:
            await cb.message.answer(badge_msg)


@router.callback_query(F.data == "step:next")
async def step_next(cb: CallbackQuery, state: FSMContext):
    tg_id = str(cb.from_user.id)
    await cb.answer()

    with contextlib.suppress(Exception):
        await cb.message.edit_reply_markup(reply_markup=None)

    async with SessionLocal() as session:
        strat, steps, day = await get_active_strategy_with_steps_by_tg(session, tg_id)
        if not strat:
            await cb.message.answer("Активная стратегия не найдена. Нажми /start и пройди анкету.")
            return

        max_day = max([int(k) for k in (steps.keys() or []) if k.isdigit()], default=0)
        if day > max_day:
            await cb.message.answer("🎉 Поздравляю! Ты выполнил все задания из стратегии!")
            return

        await ensure_today_issued(session, strat.id)
        await session.commit()

        text_msg = await _compose_today_text(strat)

        step_text = steps.get(str(day), "")
        is_simplified = _is_simplified_step(step_text)
        keyboard = kb_daily_step(day, is_simplified=is_simplified)

        await cb.message.answer(text_msg, reply_markup=keyboard)


@router.callback_query(F.data == "step:snooze")
async def step_snooze(cb: CallbackQuery, state: FSMContext):
    with contextlib.suppress(Exception):
        await cb.message.edit_reply_markup(reply_markup=None)

    tg_id = str(cb.from_user.id)
    await cb.answer("Отложили на 24 часа.")

    async with SessionLocal() as session:
        strat, steps, day = await get_active_strategy_with_steps_by_tg(session, tg_id)
        if not strat:
            await cb.message.answer("Активная стратегия не найдена. Нажми /start и пройди анкету.")
            return
        await snooze_today(session, strat.id, hours=24)
        await session.commit()

    await cb.message.answer("Ок! Напомню через 24 часа.")


@router.callback_query(F.data == "step:hint")
async def step_hint(cb: CallbackQuery, state: FSMContext):
    tg_id = str(cb.from_user.id)

    busy_key = f"hint_busy_{tg_id}"
    data_before = await state.get_data()
    if data_before.get(busy_key):
        await cb.answer()
        return

    await state.update_data({busy_key: True})
    await cb.answer()

    original_text = cb.message.html_text or cb.message.text or ""
    with contextlib.suppress(Exception):
        await cb.message.bot.edit_message_text(
            chat_id=cb.message.chat.id,
            message_id=cb.message.message_id,
            text=original_text,
            reply_markup=None,
            parse_mode="HTML",
        )

    await _typing(cb.message.bot, cb.message.chat.id, 2)

    async with SessionLocal() as session:
        strat, steps, day = await get_active_strategy_with_steps_by_tg(session, tg_id)
        if not strat:
            await state.update_data({busy_key: False})
            await cb.message.answer("Активная стратегия не найдена. Нажми /start.")
            return

        step_html = steps.get(str(day), "")
        if _is_simplified_step(step_html):
            step_html = _remove_simplified_marker(step_html)

        if isinstance(step_html, dict):
            step_html = step_html.get("html", "") or step_html.get("text", "")

        await ask_hint_today(session, strat.id)
        await session.commit()

    step_text = steps.get(str(day), "")
    is_simplified = _is_simplified_step(step_text)
    reply_markup = kb_daily_step(day, is_simplified=is_simplified)

    await cb.message.answer(step_text, reply_markup=reply_markup)
    await state.update_data({busy_key: False})


@router.callback_query(F.data == "step:fail")
async def step_fail(cb: CallbackQuery, state: FSMContext):
    with contextlib.suppress(Exception):
        await cb.message.edit_reply_markup(reply_markup=None)

    await cb.answer()
    await cb.message.answer("Почему не получилось? Выбери причину:", reply_markup=kb_fail_reason())


@router.callback_query(F.data.startswith("fail_reason:"))
async def step_fail_reason(cb: CallbackQuery, state: FSMContext):
    code = cb.data.split(":", 1)[1]
    tg_id = str(cb.from_user.id)

    if code == "cancel":
        await cb.answer("Отменено")
        async with SessionLocal() as session:
            strat, steps, day = await get_active_strategy_with_steps_by_tg(session, tg_id)

        if strat:
            text_msg = await _compose_today_text(strat)
            step_text = steps.get(str(day), "")
            is_simplified = _is_simplified_step(step_text)
            keyboard = kb_daily_step(day, is_simplified=is_simplified)

            with contextlib.suppress(Exception):
                await cb.message.edit_text(text_msg, reply_markup=keyboard)
        else:
            with contextlib.suppress(Exception):
                await cb.message.edit_reply_markup(reply_markup=kb_daily_step())
        return

    mapping = {
        "fear": FailReason.fear,
        "lazy": FailReason.laziness,
        "no_time": FailReason.no_time,
    }
    reason = mapping.get(code, FailReason.other)

    await cb.answer()

    with contextlib.suppress(Exception):
        await cb.message.edit_reply_markup(reply_markup=None)

    async with SessionLocal() as session:
        strat, steps, day = await get_active_strategy_with_steps_by_tg(session, tg_id)
        if not strat:
            await cb.message.answer("Активная стратегия не найдена. Нажми /start и пройди анкету.")
            return
        await fail_today(session, strat.id, reason=reason)
        await session.commit()

    await cb.message.answer("Ок. Зафиксировал. Завтра подберём посильный шаг.")


def _insert_split_step(
        steps: Dict[str, Any],
        day: int,
        part1: str,
        part2: str,
) -> Dict[str, Any]:
    new_steps: Dict[str, Any] = {}

    for k, v in steps.items():
        if not str(k).isdigit():
            new_steps[k] = v

    numeric_keys = sorted(int(k) for k in steps.keys() if str(k).isdigit())

    for k_int in numeric_keys:
        content = steps[str(k_int)]
        if k_int < day:
            new_steps[str(k_int)] = content
        elif k_int == day:
            new_steps[str(k_int)] = _add_simplified_marker(part1)
            new_steps[str(k_int + 1)] = _add_simplified_marker(part2)
        else:
            new_steps[str(k_int + 1)] = content

    return new_steps


@router.callback_query(F.data == "step:easier")
async def step_easier(cb: CallbackQuery, state: FSMContext):
    tg_id = str(cb.from_user.id)
    await cb.answer()

    with contextlib.suppress(Exception):
        await cb.message.edit_reply_markup(None)

    async with SessionLocal() as session:
        strat, steps, day = await get_active_strategy_with_steps_by_tg(session, tg_id)
        if not strat:
            await cb.message.answer("Активная стратегия не найдена.")
            return

        steps = steps or {}
        original_text = steps.get(str(day), "")
        if _is_simplified_step(original_text):
            original_text = _remove_simplified_marker(original_text)

        if isinstance(original_text, dict):
            original_text = original_text.get("html", "") or original_text.get("text", "")

    await _typing(cb.message.bot, cb.message.chat.id, 1)

    system_prompt = (
        "Разбей одно задание на ДВА более простых подзадания для двух дней. "
        "Сохрани суть исходного задания. "
        "Каждое подзадание должно быть четким, конкретным и выполнимым за один день. "
        "Оформи каждое подзадание в виде четкого нумерованного списка (1., 2., 3. и т.д.). "
        "Не используй общие фразы, только конкретные действия. "
        "Верни строго JSON:\n"
        '{ "part1": "...", "part2": "..." }'
    )
    user_prompt = (
        "Вот задание. Сделай его проще, разбив на два последовательных шага "
        "(на сегодня и на завтра). Сформулируй каждый шаг как четкий нумерованный список действий:\n\n"
        f"{original_text}"
    )

    try:
        result = await ask_json(
            system_prompt=system_prompt,
            user_prompt=user_prompt,
            temperature=0.1,
            max_tokens=600,
        )
        part1 = str(result.get("part1", "")).strip()
        part2 = str(result.get("part2", "")).strip()
        if not part1 or not part2:
            raise ValueError("empty parts")
    except Exception as e:
        logger.exception("step:easier split error: %s", e)
        await cb.message.answer("Не удалось упростить задание, попробуй позже.")
        return

    async with SessionLocal() as session:
        strat, steps, day = await get_active_strategy_with_steps_by_tg(session, tg_id)
        if not strat:
            await cb.message.answer("Активная стратегия не найдена.")
            return

        steps = steps or {}
        new_steps = _insert_split_step(steps, day, part1, part2)

        strat.steps = new_steps

        await ensure_today_issued(session, strat.id)
        await session.commit()

    await cb.message.answer("Ок, делаем проще! Вот первая часть задания:")
    await cb.message.answer(part1, reply_markup=kb_daily_step(day, is_simplified=True))


async def _user_tg_id(session: AsyncSession, user_id: int) -> Optional[int]:
    user = await session.scalar(select(User).where(User.id == user_id))
    if not user or not user.tg_id:
        return None
    try:
        return int(user.tg_id)
    except Exception:
        return user.tg_id


async def _compose_today_text(strat: Strategy) -> str:
    steps = strat.steps or {}
    day = strat.current_day or 1
    step = steps.get(str(day), "")

    if _is_simplified_step(step):
        step = _remove_simplified_marker(step)

    if isinstance(step, dict):
        return (
                step.get("html", "")
                or step.get("text", "")
                or "Твоя задача на сегодня готова. Открой кнопку и приступай."
        )
    return step or "Твоя задача на сегодня готова. Открой кнопку и приступай."


async def daily_broadcast_job(bot) -> None:
    async with SessionLocal() as session:
        strategies: List[Strategy] = await get_due_for_daily_issue(session)
        if not strategies:
            return

        for strat in strategies:
            try:
                chat_id = await _user_tg_id(session, strat.user_id)
                if not chat_id:
                    continue

                await ensure_today_issued(session, strat.id)
                await session.flush()

                text_msg = await _compose_today_text(strat)

                await _typing(bot, chat_id, 1)

                step_text = strat.steps.get(str(strat.current_day), "") if strat.steps else ""
                is_simplified = _is_simplified_step(step_text)
                keyboard = kb_daily_step(strat.current_day, is_simplified=is_simplified)

                await bot.send_message(chat_id, text_msg, reply_markup=keyboard)

                await asyncio.sleep(0.2)
            except Exception:
                continue

        await session.commit()


def _ensure_scheduler_started(bot) -> None:
    global _scheduler
    if _scheduler and _scheduler.running:
        return

    _scheduler = AsyncIOScheduler(timezone=TZ)
    _scheduler.add_job(
        daily_broadcast_job,
        "interval",
        minutes=minutes,
        id=JOB_ID,
        kwargs={"bot": bot},
        replace_existing=True,
        coalesce=True,
        misfire_grace_time=60,
        max_instances=1,
    )
    _scheduler.start()


def _ensure_scheduler_stopped() -> None:
    global _scheduler
    if _scheduler:
        _scheduler.shutdown(wait=False)
        _scheduler = None


@router.startup()
async def on_startup(bot):
    _ensure_scheduler_started(bot)


@router.shutdown()
async def on_shutdown():
    _ensure_scheduler_stopped()