"""One-shot scenario steps per persona (randomised inside each scenario)."""

from __future__ import annotations

import logging
import os
import random
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any, Awaitable, Callable

from . import personas
from .sim_client import SimClient

LOG = logging.getLogger(__name__)

ScenarioFn = Callable[[SimClient, "UserSimState", "RunState"], Awaitable[None]]


@dataclass
class RunState:
    """Shared across all users for one automation process."""

    company_ids: list[str] = field(default_factory=list)
    created_portal_users: list[dict[str, str]] = field(default_factory=list)
    created_staff_users: list[dict[str, str]] = field(default_factory=list)
    email_inbound_token: str | None = None
    bootstrap_password: str = "SimBootstrap123!"


@dataclass
class UserSimState:
    """Mutable per-user context across ticks."""

    email: str
    persona: str
    user_id: str | None = None
    closed_ticket_id: str | None = None
    landing_session_id: str | None = None
    inbound_token: str | None = None


def _stamp() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


def _bootstrap_password(run: RunState) -> str:
    return os.environ.get("SIM_BOOTSTRAP_PASSWORD", "").strip() or run.bootstrap_password


# --- Portal customers -------------------------------------------------------


async def end_user_portal_message(client: SimClient, state: UserSimState, _run: RunState) -> None:
    """Short everyday portal intake or follow-up."""
    open_list = await client.get_tickets(status="open", limit=10)
    use_followup = bool(open_list) and random.random() < 0.4
    suffix = random.randint(1, 999_999)
    text = f"[user_sim/end] Hi, I need help with order #{suffix}. Thanks! ({_stamp()})"
    if use_followup:
        tid = str(open_list[0]["id"])
        out = await client.post_portal_message(text, ticket_id=tid)
        LOG.info("[%s] end_user follow-up ticket=%s message=%s", state.email, tid, out.get("message", {}).get("id"))
    else:
        out = await client.post_portal_message(text)
        t = out.get("ticket") or {}
        LOG.info("[%s] end_user new/message ticket=%s", state.email, t.get("id"))


async def technical_user_portal_message(client: SimClient, state: UserSimState, _run: RunState) -> None:
    """Detail-heavy technical portal message; prefers follow-ups."""
    open_list = await client.get_tickets(status="open", limit=10)
    use_followup = bool(open_list) and random.random() < 0.7
    suffix = random.randint(1, 999_999)
    text = (
        f"[user_sim/tech] Integration failure ref={suffix} at {_stamp()}.\n"
        f"Env: staging\n"
        f"HTTP 502 from webhook callback; retry exhausted after 3 attempts.\n"
        f"Request-Id: sim-{suffix}\n"
        f"Please escalate if queue latency > 5m."
    )
    if use_followup:
        tid = str(open_list[0]["id"])
        out = await client.post_portal_message(text, ticket_id=tid)
        LOG.info("[%s] technical follow-up ticket=%s", state.email, tid)
        _ = out
    else:
        out = await client.post_portal_message(text)
        t = out.get("ticket") or {}
        LOG.info("[%s] technical new/message ticket=%s", state.email, t.get("id"))


async def portal_closed_negative(client: SimClient, state: UserSimState, _run: RunState) -> None:
    """Occasionally POST to a closed ticket id and expect HTTP 400."""
    if random.random() > 0.35:
        return
    closed = await client.get_tickets(status="closed", limit=5)
    if not closed:
        LOG.debug("[%s] closed-negative skipped (no closed tickets)", state.email)
        return
    tid = str(closed[0]["id"])
    state.closed_ticket_id = tid
    r = await client.post_portal_message_raw(
        "[user_sim] should fail — ticket closed",
        ticket_id=tid,
    )
    if r.status_code == 400:
        LOG.info("[%s] closed-ticket probe ok (400) ticket=%s", state.email, tid)
    else:
        raise RuntimeError(f"closed-ticket probe unexpected {r.status_code}: {r.text[:300]}")


# --- Staff ------------------------------------------------------------------


async def agent_ticket_touch(client: SimClient, state: UserSimState, _run: RunState) -> None:
    """List open tickets, mutate one, send a reply; rarely close."""
    open_list = await client.get_tickets(status="open", limit=25)
    if not open_list:
        LOG.info("[%s] agent: no open tickets, skipping touch", state.email)
        return

    t = random.choice(open_list)
    tid = str(t["id"])
    await client.get_ticket(tid)

    priorities = ("low", "medium", "high")
    due = (datetime.now(timezone.utc) + timedelta(days=random.randint(1, 14))).date().isoformat()

    body: dict[str, object] = {
        "priority": random.choice(priorities),
        "due_date": due,
        "needs_escalation": random.choice((True, False)),
    }
    if state.user_id and random.random() < 0.6:
        body["assigned_to"] = state.user_id

    await client.patch_ticket(tid, body)

    msg = f"[user_sim] agent note {random.randint(1, 999_999)} @ {_stamp()}"
    await client.post_ticket_message(tid, msg)

    if random.random() < 0.12:
        await client.patch_ticket(tid, {"status": "closed"})
        LOG.info("[%s] agent closed ticket %s", state.email, tid)
    else:
        LOG.info("[%s] agent touched ticket %s", state.email, tid)


async def agent_create_company(client: SimClient, state: UserSimState, run: RunState) -> None:
    """Create a company for hybrid bootstrap / coverage."""
    name = f"[user_sim] Co {random.randint(1, 999_999)} {_stamp()}"
    c = await client.create_company(name)
    cid = str(c.get("id", ""))
    if not cid:
        raise RuntimeError("create company response missing id")
    run.company_ids.append(cid)
    LOG.info("[%s] created company id=%s name=%s", state.email, cid, name)


async def _pick_company_id(client: SimClient, run: RunState) -> str:
    if run.company_ids:
        return random.choice(run.company_ids)
    existing = await client.list_companies()
    if existing:
        cid = str(existing[0]["id"])
        run.company_ids.append(cid)
        return cid
    c = await client.create_company(f"[user_sim] Co bootstrap {_stamp()}")
    cid = str(c["id"])
    run.company_ids.append(cid)
    return cid


async def agent_create_portal_user(client: SimClient, state: UserSimState, run: RunState) -> None:
    """Add a portal user to a known or newly listed company."""
    company_id = await _pick_company_id(client, run)
    suffix = uuid.uuid4().hex[:10]
    email = f"sim.portal.{suffix}@example.com"
    password = _bootstrap_password(run)
    user = await client.create_portal_user(company_id, email, password, display_name=f"Sim Portal {suffix}")
    uid = str(user.get("id", ""))
    run.created_portal_users.append({"id": uid, "email": email, "company_id": company_id})
    LOG.info("[%s] created portal user email=%s company=%s id=%s", state.email, email, company_id, uid)


async def admin_create_staff_user(client: SimClient, state: UserSimState, run: RunState) -> None:
    """Admin-only: create a staff agent (role=user)."""
    suffix = uuid.uuid4().hex[:10]
    email = f"sim.agent.{suffix}@example.com"
    password = _bootstrap_password(run)
    user = await client.create_staff_user(email, password, role="user")
    uid = str(user.get("id", ""))
    run.created_staff_users.append({"id": uid, "email": email})
    LOG.info("[%s] created staff user email=%s id=%s", state.email, email, uid)


async def admin_discover_email_token(client: SimClient, state: UserSimState, run: RunState) -> None:
    """If env token missing, try GET channel detail for first email channel (admin/staff)."""
    if run.email_inbound_token:
        return
    channels = await client.list_channels()
    email_chs = [c for c in channels if str(c.get("kind")) == "email"]
    if not email_chs:
        LOG.info("[%s] no email channels to discover inbound token", state.email)
        return
    ch = await client.get_channel(str(email_chs[0]["id"]))
    token = ch.get("inbound_token")
    if token and isinstance(token, str):
        run.email_inbound_token = token
        LOG.info("[%s] discovered email inbound_token from channel %s", state.email, ch.get("id"))
    else:
        LOG.warning("[%s] email channel %s has no inbound_token in detail", state.email, ch.get("id"))


# --- Channel actors ---------------------------------------------------------


async def email_inbound_message(client: SimClient, state: UserSimState, run: RunState) -> None:
    """POST public email inbound webhook."""
    token = state.inbound_token or run.email_inbound_token
    if not token:
        raise RuntimeError(
            "email_reporter needs inbound token: set SIM_EMAIL_INBOUND_TOKEN "
            "or run admin_discover_email_token first"
        )
    suffix = random.randint(1, 999_999)
    from_addr = state.email if "@" in state.email else f"sim.mail.{suffix}@example.com"
    subject = f"[user_sim] Email ticket {suffix}"
    text = f"Hello support,\n\nAutomated email ping {suffix} at {_stamp()}.\n\nRegards"
    out = await client.post_inbound_email(
        token,
        from_addr=from_addr,
        text=text,
        subject=subject,
        from_name="Sim Email Reporter",
        message_id=f"sim-{uuid.uuid4().hex}",
    )
    LOG.info("[%s] email inbound ok keys=%s", state.email, list(out.keys()) if isinstance(out, dict) else out)


async def landing_chat_message(client: SimClient, state: UserSimState, _run: RunState) -> None:
    """Public landing web chat; sticky sessionId. Soft-skip if captcha required."""
    suffix = random.randint(1, 999_999)
    msg = f"[user_sim/landing] Web chat ping {suffix} at {_stamp()}"
    status, body = await client.post_landing_chat(msg, session_id=state.landing_session_id)
    if status == 400 and isinstance(body, dict) and "captcha" in str(body.get("error", "")).lower():
        LOG.warning("[%s] landing chat captcha required — skip (unset TURNSTILE in local)", state.email)
        return
    if status >= 400:
        raise RuntimeError(f"landing chat failed {status}: {body}")
    if isinstance(body, dict):
        sid = body.get("sessionId") or body.get("session_id")
        if sid:
            state.landing_session_id = str(sid)
    LOG.info("[%s] landing chat ok status=%s session=%s", state.email, status, state.landing_session_id)


def scenarios_for_persona(persona: str) -> list[ScenarioFn]:
    if persona in (personas.END_USER, personas.SUPPORT_SEEKER):
        return [end_user_portal_message, portal_closed_negative]
    if persona == personas.TECHNICAL_USER:
        return [technical_user_portal_message, portal_closed_negative]
    if persona == personas.AGENT_HANDLER:
        return [
            agent_ticket_touch,
            agent_create_company,
            agent_create_portal_user,
        ]
    if persona == personas.ADMIN_OPERATOR:
        return [
            agent_ticket_touch,
            agent_create_company,
            agent_create_portal_user,
            admin_create_staff_user,
            admin_discover_email_token,
        ]
    if persona == personas.EMAIL_REPORTER:
        return [email_inbound_message]
    if persona == personas.LANDING_VISITOR:
        return [landing_chat_message]
    raise ValueError(f"no scenarios for persona {persona!r}")
