from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Any, Mapping, Protocol

from relationalai.client.errors.handlers import collect_error_messages


@dataclass(frozen=True)
class CliErrorContext:
    """Generic context for formatting CLI errors.

    Keep this intentionally generic so new services/commands don't require adding
    fields here; pass per-command values via `params`.
    """

    command: str
    params: Mapping[str, Any]


class CliErrorHandler(Protocol):
    def matches(self, error: Exception, messages: list[str], ctx: CliErrorContext) -> bool: ...
    def format_details(self, error: Exception, ctx: CliErrorContext) -> str: ...


def _contains_any(messages: list[str], needles: tuple[str, ...]) -> bool:
    def _norm(s: str) -> str:
        # Snowflake/DA wrappers can inject newlines/indentation/box-drawing chars.
        # Normalize to a whitespace-collapsed lowercase string for robust matching.
        return " ".join(str(s).split()).lower()

    ns = tuple(_norm(n) for n in needles if isinstance(n, str) and n)
    if not ns:
        return False

    for msg in messages:
        if not isinstance(msg, str) or not msg:
            continue
        m = _norm(msg)
        if any(n in m for n in ns):
            return True
    return False


def _extract_service_message(text: str) -> str | None:
    """Best-effort extract of the backend 'message' field."""
    if not text:
        return None

    m = re.search(r'"message"\s*:\s*"([^"]+)"', text, flags=re.IGNORECASE)
    if m:
        return m.group(1).strip()

    m = re.search(r"bad request\s*-\s*([^\(\n]+)", text, flags=re.IGNORECASE)
    if m:
        return m.group(1).strip()

    return None


def _replace_engine_words(text: str) -> str:
    # We may need to *match* backend strings containing "engine", but we should not
    # show that word to users in CLI output.
    out = re.sub(r"\bengine\b", "reasoner", text, flags=re.IGNORECASE)
    out = re.sub(r"\bengines\b", "reasoners", out, flags=re.IGNORECASE)
    return out


def _param(ctx: CliErrorContext, key: str) -> str | None:
    v = ctx.params.get(key)
    if v is None:
        return None
    s = str(v).strip()
    return s or None


def _reasoner_type_name(ctx: CliErrorContext) -> tuple[str | None, str | None]:
    reasoner_type = _param(ctx, "job_type") or _param(ctx, "reasoner_type") or _param(ctx, "type")
    reasoner_name = _param(ctx, "reasoner_name") or _param(ctx, "name")
    return reasoner_type, reasoner_name


def _reasoner_type_label(reasoner_type: str | None) -> str | None:
    """Prefer CLI-facing reasoner type labels (Logic/Prescriptive/Predictive)."""
    if reasoner_type is None:
        return None
    s = str(reasoner_type).strip()
    if not s:
        return None
    # Jobs service uses SOLVER/ML as type discriminators; keep CLI labels stable.
    try:
        from relationalai.tools.cli.job_types import job_type_label

        jl = job_type_label(s)
        if jl:
            return jl
    except Exception:
        pass
    try:
        from relationalai.services.reasoners.models import ReasonerType

        return ReasonerType.get_label(ReasonerType.normalize(s))
    except Exception:
        return s


def _green(cmd: str) -> str:
    return f"[green]{cmd}[/green]"


class TransactionEventsNotFoundForUserHandler:
    _NEEDLES = (
        "GET_TRANSACTION_EVENTS",
        "transaction",
        "not found for user",
        '"status":"Not Found"',
    )

    _TX_NOT_FOUND_RE = re.compile(
        r'transaction\s+"(?P<transaction_id>[^"]+)"\s+not found for user\s+"(?P<user>[^"]+)"',
        flags=re.IGNORECASE,
    )

    def matches(self, error: Exception, messages: list[str], ctx: CliErrorContext) -> bool:  # noqa: ARG002
        # Only relevant for the jobs events command (LOGIC transaction events).
        if str(getattr(ctx, "command", "") or "").strip() != "jobs:events":
            return False
        return _contains_any(messages, self._NEEDLES)

    def format_details(self, error: Exception, ctx: CliErrorContext) -> str:
        raw = str(error) or repr(error)
        # Snowflake wrapper errors often escape quotes as `\"` inside an embedded JSON string.
        raw_norm = raw.replace('\\"', '"')
        m = self._TX_NOT_FOUND_RE.search(raw_norm)
        transaction_id = m.group("transaction_id").strip() if m else None
        user = m.group("user").strip() if m else None

        job_id = _param(ctx, "job_id") or _param(ctx, "id")
        reasoner_type, _reasoner_name = _reasoner_type_name(ctx)
        reasoner_type_label = _reasoner_type_label(reasoner_type)

        lines: list[str] = ["Problem: job events were not found for the current user."]
        if job_id:
            lines.append(f"Job ID: {job_id}")
        if reasoner_type_label:
            lines.append(f"Type: {reasoner_type_label}")
        if transaction_id:
            lines.append(f"Transaction: {transaction_id}")
        if user:
            lines.append(f"User: {user}")

        lines.append("")
        lines.append("Note: job IDs are only valid for the user that created the job.")
        lines.append("Fix: choose a job you created, then retry.")
        if user:
            lines.append("  " + _green(f"rai jobs:list --created-by {user}"))
        else:
            lines.append("  " + _green("rai jobs:list"))

        return "\n".join(lines)


class InvalidJobTypeForReasonerHandler:
    _NEEDLES = ("CREATE_JOB", "invalid job type")

    def matches(self, error: Exception, messages: list[str], ctx: CliErrorContext) -> bool:  # noqa: ARG002
        # Keep this narrow: many failures may mention CREATE_JOB, but we only want
        # the explicit "invalid job type" case.
        hay = " ".join(" ".join(str(m).split()).lower() for m in messages if isinstance(m, str))
        if "invalid job type" not in hay:
            return False
        if "create_job" not in hay:
            return False
        # Don't overtake more specific reasoner resource errors.
        if ("engine not found" in hay) or ("reasoner not found" in hay):
            return False
        return True

    def format_details(self, error: Exception, ctx: CliErrorContext) -> str:
        raw = str(error) or repr(error)
        service_msg = _extract_service_message(raw) or "invalid job type"

        reasoner_type, reasoner_name = _reasoner_type_name(ctx)
        reasoner_type_label = _reasoner_type_label(reasoner_type)

        lines: list[str] = ["Problem: job type does not match the reasoner type."]
        lines.append(f"Details: {service_msg}.")
        if reasoner_name:
            lines.append(f"Reasoner: {reasoner_name}")
        if reasoner_type_label:
            lines.append(f"Job type: {reasoner_type_label}")

        lines.append("")
        lines.append("Fix: use the reasoner's actual type with `--type`.")
        if reasoner_name:
            lines.append("  " + _green(f"rai reasoners:list --name {reasoner_name}"))
        lines.append("  " + _green("rai jobs:create --type <Logic|Prescriptive|Predictive> --reasoner-name <name> ..."))

        return "\n".join(lines)


class TransactionPayloadMissingRequiredFieldsHandler:
    _NEEDLES = (
        "transaction create requires payload",
    )

    def matches(self, error: Exception, messages: list[str], ctx: CliErrorContext) -> bool:  # noqa: ARG002
        if str(getattr(ctx, "command", "") or "").strip() != "jobs:create":
            return False
        return _contains_any(messages, self._NEEDLES)

    def format_details(self, error: Exception, ctx: CliErrorContext) -> str:
        reasoner_type, reasoner_name = _reasoner_type_name(ctx)
        reasoner_type_label = _reasoner_type_label(reasoner_type)

        lines: list[str] = ["Problem: you selected Logic jobs (transactions) but the payload is missing required fields."]
        if reasoner_name:
            lines.append(f"Reasoner: {reasoner_name}")
        if reasoner_type_label:
            lines.append(f"Job type: {reasoner_type_label}")

        lines.append("")
        lines.append("Fix:")
        lines.append("- If you meant a Prescriptive job, use `--type Prescriptive`")
        lines.append("- If you meant a Logic transaction, provide `--database` and `--raw-code` (or a payload with dbname/query).")

        if reasoner_name:
            lines.append("")
            lines.append("Examples:")
            lines.append("  " + _green(f"rai jobs:create --type Prescriptive --name {reasoner_name} --payload-json ..."))
            lines.append("  " + _green(f"rai jobs:create --type Logic --name {reasoner_name} --database DB --raw-code '...'"))

        return "\n".join(lines)


class ReasonerAlreadyExistsHandler:
    def matches(self, error: Exception, messages: list[str], ctx: CliErrorContext) -> bool:  # noqa: ARG002
        # Be tolerant: backend/wrappers may change punctuation/casing.
        hay = " ".join(m for m in messages if isinstance(m, str)).lower()
        return ("reasoner already exists" in hay) or ("already exists but does not match requested" in hay)

    def format_details(self, error: Exception, ctx: CliErrorContext) -> str:
        raw = str(error) or repr(error)

        reasoner_type, reasoner_name = _reasoner_type_name(ctx)
        reasoner_type_label = _reasoner_type_label(reasoner_type)
        requested_size = _param(ctx, "reasoner_size") or _param(ctx, "size")

        # Best-effort state extraction from common conflict string.
        state = None
        m = re.search(r"\bstate=([A-Z_]+)\b", raw)
        if m:
            state = m.group(1).strip()

        lines: list[str] = ["Problem: a reasoner with the same name and type already exists."]
        if reasoner_name:
            lines.append(f"Reasoner: {reasoner_name}")
        if reasoner_type_label:
            lines.append(f"Type: {reasoner_type_label}")
        if state:
            lines.append(f"State: {state}")

        lines.append("")
        if requested_size:
            lines.append(f"Requested size: {requested_size}")
        lines.append("Note: reasoner size cannot be changed in-place.")
        lines.append("Fix: delete the existing reasoner, then create it again with the desired size.")

        if reasoner_type_label and reasoner_name:
            lines.append("  " + _green(f"rai reasoners:delete --type {reasoner_type_label} --name {reasoner_name}"))
            if requested_size:
                lines.append(
                    "  "
                    + _green(
                        f"rai reasoners:create --type {reasoner_type_label} --name {reasoner_name} --size {requested_size}"
                    )
                )
            else:
                lines.append(
                    "  " + _green(f"rai reasoners:create --type {reasoner_type_label} --name {reasoner_name}")
                )

        return "\n".join(lines)


class SuspendedReasonerHandler:
    _NEEDLES = ("engine is suspended", "reasoner is suspended")

    def matches(self, error: Exception, messages: list[str], ctx: CliErrorContext) -> bool:  # noqa: ARG002
        return _contains_any(messages, self._NEEDLES)

    def format_details(self, error: Exception, ctx: CliErrorContext) -> str:
        raw = str(error) or repr(error)
        service_msg = _extract_service_message(raw) or "reasoner is suspended"
        service_msg = _replace_engine_words(service_msg)

        reasoner_type, reasoner_name = _reasoner_type_name(ctx)
        reasoner_type_label = _reasoner_type_label(reasoner_type)

        lines: list[str] = [f"Problem: {service_msg}."]
        if reasoner_name:
            lines.append(f"Reasoner: {reasoner_name}")
        if reasoner_type_label:
            lines.append(f"Type: {reasoner_type_label}")

        if reasoner_type_label and reasoner_name:
            lines.append("")
            lines.append("Fix: resume the reasoner, then retry.")
            lines.append("  " + _green(f"rai reasoners:resume --type {reasoner_type_label} --name {reasoner_name}"))

        return "\n".join(lines)


class ReasonerNotFoundHandler:
    _NEEDLES = ("engine not found", "reasoner not found")

    def matches(self, error: Exception, messages: list[str], ctx: CliErrorContext) -> bool:
        return _contains_any(messages, self._NEEDLES)

    def format_details(self, error: Exception, ctx: CliErrorContext) -> str:
        raw = str(error) or repr(error)
        service_msg = _extract_service_message(raw) or "reasoner not found"
        service_msg = _replace_engine_words(service_msg)

        reasoner_type, reasoner_name = _reasoner_type_name(ctx)
        reasoner_type_label = _reasoner_type_label(reasoner_type)

        lines: list[str] = [f"Problem: {service_msg}."]
        if reasoner_name:
            lines.append(f"Reasoner: {reasoner_name}")
        if reasoner_type_label:
            lines.append(f"Type: {reasoner_type_label}")

        if reasoner_type_label and reasoner_name:
            lines.append("")
            lines.append("Fix: create the reasoner, then retry.")
            lines.append("  " + _green(f"rai reasoners:create --type {reasoner_type_label} --name {reasoner_name}"))
        else:
            lines.append("")
            lines.append("Fix: create the reasoner, then retry.")
            lines.append(f"  {_green('rai reasoners:create')}")

        return "\n".join(lines)


@dataclass(frozen=True)
class CliErrorHandlerChain:
    handlers: list[CliErrorHandler]

    def format_details(self, error: Exception, ctx: CliErrorContext) -> str:
        # Use the SDK's robust message collector (includes args/cause/context and
        # extracts embedded JSON message fields in Snowflake wrapper errors).
        messages = collect_error_messages(error)

        for h in self.handlers:
            try:
                if h.matches(error, messages, ctx):
                    return h.format_details(error, ctx)
            except Exception:
                continue

        return str(error) or repr(error)


DEFAULT_CLI_ERROR_HANDLER_CHAIN = CliErrorHandlerChain(
    handlers=[
        TransactionEventsNotFoundForUserHandler(),
        InvalidJobTypeForReasonerHandler(),
        TransactionPayloadMissingRequiredFieldsHandler(),
        ReasonerAlreadyExistsHandler(),
        SuspendedReasonerHandler(),
        ReasonerNotFoundHandler(),
    ]
)

