# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""Rich dispatcher for TIRx operator dispatchs.

This module adds a structured dispatch table with predicates and
deterministic failure reporting via exceptions.
"""

from __future__ import annotations

import traceback
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from tvm.ir import Op
from tvm.tirx import PrimFunc
from tvm.tirx.operator import get_tirx_op
from tvm.tirx.stmt import TilePrimitiveCall

from .dispatch_context import DispatchContext


class DispatchFail(RuntimeError):
    """Raised by variants or predicates to provide a reasoned failure."""


@dataclass
class Predicate:
    """A named predicate. The callable can return:

    - bool
    - (bool, str) where the second element is an optional reason on failure
    - raise DispatchFail(reason)
    """

    name: str
    fn: Callable[[TilePrimitiveCall, DispatchContext], Any]
    kwargs: dict[str, Any]

    def evaluate(
        self, op_call: TilePrimitiveCall, sctx: DispatchContext
    ) -> tuple[bool, str | None]:
        try:
            out = self.fn(op_call, sctx, **self.kwargs)
            if isinstance(out, tuple):
                ok, reason = out
                return bool(ok), (str(reason) if not ok and reason is not None else None)
            return bool(out), None
        except DispatchFail as e:  # surface explicit failure reasons
            return False, str(e)
        except Exception as e:  # unexpected predicate exception
            return False, f"predicate exception: {type(e).__name__}: {e}"


def predicate(
    name: str, fn: Callable[[TilePrimitiveCall, DispatchContext], Any], **kwargs
) -> Predicate:
    """Wrap a callable into a named predicate."""

    return Predicate(name=name, fn=fn, kwargs=kwargs)


def fail(reason: str) -> None:
    """Helper for schedule variants to explain why they decline to handle the op."""

    raise DispatchFail(reason)


@dataclass
class DispatchCase:
    variant: str
    priority: int
    preds: list[Predicate]
    # Impl must either return a PrimFunc or raise DispatchFail
    impl: Callable[[TilePrimitiveCall, DispatchContext], PrimFunc]


# Keyed by (Op, target_kind)
_DISPATCH_TABLE: dict[tuple[Op, str], list[DispatchCase]] = {}


def _target_kind_name(sctx: DispatchContext) -> str:
    """Normalize target kind to a stable dispatch key."""

    kind = getattr(getattr(sctx, "target", None), "kind", None)
    return getattr(kind, "name", str(kind))


def register_dispatch(
    op_name: str,
    target_kind: str,
    *,
    variant: str,
    priority: int = 0,
    when: list[Predicate] | None = None,
):
    """Decorator to add a dispatch case for an op/target pair.

    Cases with higher priority run earlier. When list predicates must all pass.
    The impl must return a PrimFunc on success, and must NOT return None.
    To decline handling, raise `fail("reason")` (or `DispatchFail`).
    """

    op = get_tirx_op(op_name)

    def decorator(impl: Callable[[TilePrimitiveCall, DispatchContext], Any]):
        # Wrap impl to forbid returning None; require raise-or-PrimFunc
        def wrapped_impl(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
            res = impl(op_call, sctx)
            if res is None:
                # Enforce raise-or-PrimFunc contract for schedule implementations
                raise DispatchFail(
                    "impl returned None; schedule must return PrimFunc or raise fail()"
                )
            return res  # type: ignore[return-value]

        cases = _DISPATCH_TABLE.setdefault((op, target_kind), [])
        cases.append(
            DispatchCase(variant=variant, priority=priority, preds=when or [], impl=wrapped_impl)
        )
        return impl

    return decorator


def list_registered_schedules() -> dict[str, dict[str, list[str]]]:
    """Return a mapping: op_name -> target_kind -> [variant names]."""

    out: dict[str, dict[str, list[str]]] = {}
    for (op, tgt), cases in _DISPATCH_TABLE.items():
        name = op.name
        out.setdefault(name, {}).setdefault(tgt, [])
        # keep insertion order by default; sort by priority desc for readability
        for c in sorted(cases, key=lambda x: (-x.priority, x.variant)):
            out[name][tgt].append(c.variant)
    return out


def _format_opcall(op_call: TilePrimitiveCall) -> str:
    """Return a readable representation of the failing opcall."""
    # Prefer TVMScript or IR text printer if available on this object
    try:
        script_method = getattr(op_call, "script", None)
        if callable(script_method):
            try:
                return str(script_method())
            except TypeError:
                # Some versions may require keyword args; fall back safely
                return str(script_method())
        astext_method = getattr(op_call, "astext", None)
        if callable(astext_method):
            return str(astext_method())
    except Exception:
        pass
    try:
        s = str(op_call)
        # constrain extremely long single-line prints from repr
        return s
    except Exception:
        pass
    try:
        args_len = len(getattr(op_call, "args", []))
    except Exception:
        args_len = -1
    try:
        op_name = op_call.op.name  # type: ignore[attr-defined]
    except Exception:
        op_name = "<unknown-op>"
    return f"op={op_name}, args={args_len}"


def _format_failure_table(header: str, rows: list[tuple[str, list[str]]]) -> str:
    """Format failures into a readable ASCII table.

    Parameters
    ----------
    header : str
        The header line describing the op/target
    rows : List[Tuple[str, str, Optional[str]]]
        Each row is (variant_label, error_summary, traceback_str)

    Returns
    -------
    str
        The formatted report string
    """
    # Compute column widths
    variant_header = "Variant"
    error_header = "Error"
    variant_col_w = (
        max(len(variant_header), *(len(v) for (v, _) in rows)) if rows else len(variant_header)
    )
    # Error column width needs to consider multi-line cells
    if rows:
        error_col_w = max(
            len(error_header), *(max(len(line) for line in errs) for (_, errs) in rows)
        )
    else:
        error_col_w = len(error_header)

    def hline(sep: str = "+") -> str:
        return f"{sep}{'-' * (variant_col_w + 2)}{sep}{'-' * (error_col_w + 2)}{sep}"

    lines: list[str] = [header]
    if not rows:
        # No rows; keep the header only
        return "\n".join(lines)

    # Table header
    lines.append(hline("+"))
    lines.append(f"| {variant_header.ljust(variant_col_w)} | {error_header.ljust(error_col_w)} |")
    lines.append(hline("+"))

    # Rows (support multi-line Error column)
    for variant, errs in rows:
        if not errs:
            errs = [""]
        for i, err_line in enumerate(errs):
            v_text = variant if i == 0 else ""
            lines.append(f"| {v_text.ljust(variant_col_w)} | {err_line.ljust(error_col_w)} |")
    lines.append(hline("+"))

    return "\n".join(lines)


def run_dispatch(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc | None:
    """Run structured dispatch.

    Returns a PrimFunc on success. Otherwise, raises RuntimeError with
    an aggregated reason report.
    """

    target_kind = _target_kind_name(sctx)
    key = (op_call.op, target_kind)
    cases = _DISPATCH_TABLE.get(key)
    if not cases:
        header = f"TIRx schedule dispatch failed: op={op_call.op.name} target={target_kind}"
        report = _format_failure_table(header, [])
        # Append a simple reason when there are no variants at all
        report = "\n".join([report, "no registered variants for this op/target"])
        raise RuntimeError(report)

    # Collect structured failure rows: (variant_label, error_lines)
    # error_lines: [summary, traceback lines...]
    failure_rows: list[tuple[str, list[str]]] = []
    last_exception: BaseException | None = None

    # If explicit dispatch is set, filter to that variant only
    forced_variant = getattr(op_call, "dispatch", None)
    if forced_variant is not None:
        cases = [c for c in cases if c.variant == forced_variant]
        if not cases:
            msg_header = f"TIRx schedule dispatch failed: op={op_call.op.name} target={target_kind}"
            table = _format_failure_table(msg_header, [])
            msg = "\n".join([table, f"no variant named '{forced_variant}' is registered"])
            raise RuntimeError(msg)

    for case in sorted(cases, key=lambda c: (-c.priority, c.variant)):
        # evaluate predicates
        pred_ok = True
        pred_msgs: list[str] = []
        for pred in case.preds:
            ok, reason = pred.evaluate(op_call, sctx)
            if not ok:
                pred_ok = False
                msg = f"rejected: {pred.name}"
                if reason:
                    msg += f" — {reason}"
                pred_msgs.append(msg)
        if not pred_ok:
            # Include the offending TilePrimitiveCall IR in the error cell
            op_str = _format_opcall(op_call)
            op_lines = [line.rstrip("\n") for line in str(op_str).splitlines()] if op_str else []
            failure_rows.append(
                (
                    f"{case.variant} (prio={case.priority})",
                    ["; ".join(pred_msgs), "opcall:", *op_lines],
                )
            )
            continue

        # run impl
        try:
            res = case.impl(op_call, sctx)
            # Defensive check in case a legacy impl bypassed the wrapper
            if res is None:  # pragma: no cover - legacy guard
                raise DispatchFail("impl returned None (legacy behavior not allowed)")
            return res
        except DispatchFail as e:
            op_str = _format_opcall(op_call)
            op_lines = [line.rstrip("\n") for line in str(op_str).splitlines()] if op_str else []
            failure_rows.append(
                (
                    f"{case.variant} (prio={case.priority})",
                    [f"declined — {e!s}", "opcall:", *op_lines],
                )
            )
        except Exception as e:  # keep searching other variants
            exc_summary = f"exception — {type(e).__name__}: {e}"
            tb_str = "".join(traceback.format_exception(type(e), e, e.__traceback__))
            # Expand traceback into lines
            tb_lines = [line.rstrip("\n") for line in tb_str.splitlines()]
            op_str = _format_opcall(op_call)
            op_lines = [line.rstrip("\n") for line in str(op_str).splitlines()] if op_str else []
            error_lines = [exc_summary, "opcall:", *op_lines, *tb_lines]
            failure_rows.append((f"{case.variant} (prio={case.priority})", error_lines))
            last_exception = e

    # no success
    header = f"TIRx schedule dispatch failed: op={op_call.op.name} target={target_kind}"
    report = _format_failure_table(header, failure_rows)
    if last_exception is not None:
        raise RuntimeError(report) from last_exception
    raise RuntimeError(report)
