"""Discord bot (gateway) — interaktivní EV zprávy se dvěma výběry.

Architektura: websocket spojení jde Z BACKENDU VEN (žádný veřejný endpoint,
EMS zůstává na VPN). Bot reaguje výhradně na whitelisted user ID a jediné,
co umí, je patch otevřené EV session + okamžitý replan — žádné režimy,
žádné registry. Bez DISCORD_BOT_TOKEN modul tiše spí (fáze A webhook).

UI: dva persistent Selecty (custom_id template, takže fungují i po
restartu backendu — obsluha jde přes on_interaction + regex, ne přes
zaregistrovanou View instanci):
  ev:<site_id>:<charger_code>:dep — „Kdy odjíždíš?"
      za 2 h | za 4 h | dnes večer 18:00 | zítra ráno 7:00 |
      zítra poledne 12:00 | pondělí ráno 7:00
  ev:<site_id>:<charger_code>:tgt — „Kolik potřebuješ?"
      30 % | 50 % | 70 % | 100 % | Nenabíjet (target = SoC při připojení)

Každý výběr okamžitě PATCHne session (fn_ev_session_apply_patch) jen v dané
dimenzi — druhý rozměr zůstává (default z ems.fn_ev_session_defaults nebo
předchozí výběr). Legacy tlačítka h2/h4/morning/full/stop ze starších zpráv
zůstávají obsloužená (action_to_patch).

Postup zřízení bota: docs/discord-ev-interaction.md.
"""

from __future__ import annotations

import asyncio
import logging
import re
from datetime import datetime, timedelta, timezone
from typing import Any
from zoneinfo import ZoneInfo

import asyncpg

from app.config import get_settings

logger = logging.getLogger(__name__)

_PRAGUE = ZoneInfo("Europe/Prague")
_POOL: asyncpg.Pool | None = None
_CLIENT: Any = None  # discord.Client za lazy importem

CUSTOM_ID_RE = re.compile(
    r"^ev:(?P<site>\d+):(?P<charger>[a-z0-9\-]+)"
    r":(?P<action>dep|tgt|h2|h4|morning|full|stop)$"
)

#: Výběr 1 — „Kdy odjíždíš?" (value, label); absolutní čas viz
#: departure_choice_to_deadline (Europe/Prague).
DEP_CHOICES: list[tuple[str, str]] = [
    ("h2", "za 2 h"),
    ("h4", "za 4 h"),
    ("today18", "dnes večer 18:00"),
    ("tomorrow7", "zítra ráno 7:00"),
    ("tomorrow12", "zítra poledne 12:00"),
    ("monday7", "pondělí ráno 7:00"),
]

#: Výběr 2 — „Kolik potřebuješ?" (value, label); "stop" = nenabíjet.
TGT_CHOICES: list[tuple[str, str]] = [
    ("30", "30 %"),
    ("50", "50 %"),
    ("70", "70 %"),
    ("100", "100 %"),
    ("stop", "Nenabíjet"),
]

#: Legacy tlačítka (starší zprávy poslané před přechodem na selecty).
LEGACY_ACTION_LABELS = {
    "h2": "za 2 h",
    "h4": "za 4 h",
    "morning": "ráno",
    "full": "do plna",
    "stop": "nenabíjet",
}


def parse_custom_id(cid: str) -> tuple[int, str, str] | None:
    m = CUSTOM_ID_RE.match(cid or "")
    if not m:
        return None
    return int(m.group("site")), m.group("charger"), m.group("action")


def departure_choice_to_deadline(choice: str, *, now: datetime) -> datetime:
    """Absolutní deadline (Europe/Prague) pro volbu z výběru „Kdy odjíždíš?".

    Čisté a testovatelné. Volby s pevným časem znamenají NEJBLIŽŠÍ budoucí
    výskyt (dnes 18:00 po 18. hodině → zítra 18:00; pondělí 7:00 z pátku je
    explicitní volba — smí být i za >48 h).
    """
    local = now.astimezone(_PRAGUE)
    if choice == "h2":
        return local + timedelta(hours=2)
    if choice == "h4":
        return local + timedelta(hours=4)
    if choice == "today18":
        candidate = local.replace(hour=18, minute=0, second=0, microsecond=0)
        if candidate <= local:
            candidate += timedelta(days=1)
        return candidate
    if choice == "tomorrow7":
        return (local + timedelta(days=1)).replace(
            hour=7, minute=0, second=0, microsecond=0
        )
    if choice == "tomorrow12":
        return (local + timedelta(days=1)).replace(
            hour=12, minute=0, second=0, microsecond=0
        )
    if choice == "monday7":
        candidate = local.replace(hour=7, minute=0, second=0, microsecond=0)
        candidate += timedelta(days=(0 - local.weekday()) % 7)  # 0 = pondělí
        if candidate <= local:
            candidate += timedelta(days=7)
        return candidate
    raise ValueError(f"unknown departure choice {choice}")


def select_to_patch(
    kind: str,
    value: str,
    *,
    now: datetime,
    soc_at_connect: float | None,
) -> dict:
    """Patch pro fn_ev_session_apply_patch z hodnoty selectu (čisté, testovatelné).

    Patchuje VŽDY jen jednu dimenzi — druhá zůstává beze změny
    (default z fn_ev_session_defaults, případně dřívější výběr).
    """
    if kind == "dep":
        deadline = departure_choice_to_deadline(value, now=now)
        return {"target_deadline": deadline.isoformat()}
    if kind == "tgt":
        if value == "stop":
            return {"target_soc_pct": float(soc_at_connect or 0)}
        return {"target_soc_pct": float(value)}
    raise ValueError(f"unknown select kind {kind}")


def choice_label(kind: str, value: str) -> str:
    """Lidský popisek volby pro potvrzení ve zprávě."""
    if kind == "dep":
        return "odjezd " + dict(DEP_CHOICES).get(value, value)
    if kind == "tgt":
        if value == "stop":
            return "nenabíjet"
        return "cíl " + dict(TGT_CHOICES).get(value, value)
    return LEGACY_ACTION_LABELS.get(value, value)


def action_to_patch(
    action: str,
    *,
    now: datetime,
    soc_at_connect: float | None,
    default_deadline_hour: int | None,
) -> dict:
    """Patch pro legacy tlačítka h2/h4/morning/full/stop (starší zprávy)."""
    if action == "h2":
        return {"target_deadline": (now + timedelta(hours=2)).isoformat()}
    if action == "h4":
        return {"target_deadline": (now + timedelta(hours=4)).isoformat()}
    if action == "morning":
        hour = int(default_deadline_hour or 7)
        local = now.astimezone(_PRAGUE)
        candidate = local.replace(hour=hour, minute=0, second=0, microsecond=0)
        if candidate <= local:
            candidate += timedelta(days=1)
        return {"target_deadline": candidate.isoformat()}
    if action == "full":
        return {
            "target_soc_pct": 100,
            "target_deadline": (now + timedelta(hours=1)).isoformat(),
        }
    if action == "stop":
        return {"target_soc_pct": float(soc_at_connect or 0)}
    raise ValueError(f"unknown action {action}")


def set_pool(pool: asyncpg.Pool) -> None:
    global _POOL
    _POOL = pool


def _allowed_user_ids() -> set[int]:
    raw = (getattr(get_settings(), "discord_allowed_user_ids", "") or "").strip()
    out: set[int] = set()
    for part in raw.split(","):
        part = part.strip()
        if part.isdigit():
            out.add(int(part))
    return out


def _build_view(site_id: int, charger_code: str):
    """View se dvěma selecty (persistent custom_id, timeout=None)."""
    import discord

    view = discord.ui.View(timeout=None)
    view.add_item(
        discord.ui.Select(
            custom_id=f"ev:{site_id}:{charger_code}:dep",
            placeholder="🕑 Kdy odjíždíš?",
            min_values=1,
            max_values=1,
            options=[
                discord.SelectOption(label=label, value=value)
                for value, label in DEP_CHOICES
            ],
        )
    )
    view.add_item(
        discord.ui.Select(
            custom_id=f"ev:{site_id}:{charger_code}:tgt",
            placeholder="🔋 Kolik potřebuješ?",
            min_values=1,
            max_values=1,
            options=[
                discord.SelectOption(label=label, value=value)
                for value, label in TGT_CHOICES
            ],
        )
    )
    return view


async def post_ev_arrival(
    site_id: int, charger_code: str, session_id: int, text: str
) -> bool:
    """Pošle zprávu s výběry přes bota. False = bot neběží/není kanál (fallback webhook)."""
    if _CLIENT is None or not _CLIENT.is_ready():
        return False
    channel_id = int(getattr(get_settings(), "discord_ev_channel_id", 0) or 0)
    if not channel_id:
        return False
    channel = _CLIENT.get_channel(channel_id)
    if channel is None:
        return False
    await channel.send(content=text, view=_build_view(site_id, charger_code))
    return True


async def _handle_action(
    interaction: Any,
    site_id: int,
    charger_code: str,
    action: str,
    value: str | None,
) -> None:
    import json

    from services.control_exporter import export_setpoints
    from services.ev_notify import build_ev_plan_summary, get_open_session
    from services.planning_engine import run_rolling_replan

    assert _POOL is not None
    async with _POOL.acquire() as conn:
        sess = await get_open_session(site_id, charger_code, conn)
        if sess is None:
            await interaction.followup.send(
                "Session už není otevřená (auto odpojeno?).", ephemeral=True
            )
            return
        now = datetime.now(timezone.utc)
        if action in ("dep", "tgt"):
            if not value:
                return
            patch = select_to_patch(
                action,
                value,
                now=now,
                soc_at_connect=sess["soc_at_connect_pct"],
            )
            label = choice_label(action, value)
        else:  # legacy tlačítka starších zpráv
            patch = action_to_patch(
                action,
                now=now,
                soc_at_connect=sess["soc_at_connect_pct"],
                default_deadline_hour=sess["default_deadline_hour"],
            )
            label = LEGACY_ACTION_LABELS.get(action, action)
        await conn.fetchval(
            "select ems.fn_ev_session_apply_patch($1::int, $2::int, $3::jsonb)",
            site_id,
            int(sess["session_id"]),
            json.dumps(patch),
        )
        await run_rolling_replan(
            site_id, conn, triggered_by=f"discord:{action}:{charger_code}"
        )
        await export_setpoints(site_id, conn)
        new_text = await build_ev_plan_summary(site_id, charger_code, conn)

    if new_text:
        await interaction.message.edit(
            content=new_text + f"\n_(nastaveno: {label})_",
            view=_build_view(site_id, charger_code),
        )
    await interaction.followup.send(f"Přeplánováno ✓ ({label})", ephemeral=True)


async def run_discord_bot() -> None:
    """Lifespan task: připojí gateway a obsluhuje selecty/tlačítka. Bez tokenu hned končí."""
    token = (getattr(get_settings(), "discord_bot_token", "") or "").strip()
    if not token:
        logger.info("Discord bot: token není nastaven — fáze B vypnuta")
        return
    import discord

    intents = discord.Intents.default()
    client = discord.Client(intents=intents)

    @client.event
    async def on_ready() -> None:
        logger.info("Discord bot připojen jako %s", client.user)
        try:
            channel_id = int(getattr(get_settings(), "discord_ev_channel_id", 0) or 0)
            ch = client.get_channel(channel_id) if channel_id else None
            if ch is not None:
                await ch.send("✅ EMS bot online — notifikace aktivní")
        except Exception:
            logger.exception("Discord on_ready ping failed")

    @client.event
    async def on_interaction(interaction: discord.Interaction) -> None:
        if interaction.type != discord.InteractionType.component:
            return
        data = interaction.data or {}
        cid = data.get("custom_id", "")
        parsed = parse_custom_id(str(cid))
        if parsed is None:
            return
        allowed = _allowed_user_ids()
        if allowed and interaction.user.id not in allowed:
            await interaction.response.send_message(
                "Tenhle výběr není pro tebe. 🙂", ephemeral=True
            )
            return
        await interaction.response.defer(ephemeral=True, thinking=True)
        site_id, charger_code, action = parsed
        values = data.get("values") or []
        value = str(values[0]) if values else None
        try:
            await _handle_action(interaction, site_id, charger_code, action, value)
        except Exception:
            logger.exception("Discord akce selhala (%s, value=%s)", cid, value)
            try:
                await interaction.followup.send(
                    "Akce selhala — mrkni do logů.", ephemeral=True
                )
            except Exception:
                pass

    global _CLIENT
    _CLIENT = client
    try:
        await client.start(token)
    except asyncio.CancelledError:
        await client.close()
        raise
    except Exception:
        logger.exception("Discord bot spadl — fáze B mimo provoz (fallback webhook)")
    finally:
        _CLIENT = None
