from __future__ import annotations

from collections.abc import Callable
from typing import Any

try:
    from .common import (
        build_input_tensor,
        build_standard_argparser,
        ensure_npu_available,
        get_replay_repeat_count,
        get_runtime_modules,
        get_target_data_dir,
        init_runtime,
        normalize_dtype_name,
        parse_list_field,
        parse_shape,
        print_invalid_replay_summary,
        process_replay_csvs,
    )
except ImportError:
    from common import (
        build_input_tensor,
        build_standard_argparser,
        ensure_npu_available,
        get_replay_repeat_count,
        get_runtime_modules,
        get_target_data_dir,
        init_runtime,
        normalize_dtype_name,
        parse_list_field,
        parse_shape,
        print_invalid_replay_summary,
        process_replay_csvs,
    )


class OpReplay:
    def __init__(
        self,
        *,
        kernel_type: str,
        api_path: str | None = None,
        description: str,
        usage_examples: list[str],
        version_help: str,
        input_count: int | None = None,
        fixed_kwargs: dict[str, Any] | None = None,
        input_dtype_overrides: dict[int, str] | None = None,
        prepare: Callable[[], None] | None = None,
        build_case: Callable[[dict[str, str]], dict[str, Any]] | None = None,
        run_case: Callable[[dict[str, Any]], Any] | None = None,
        format_success: Callable[[str, int, dict[str, str], dict[str, Any], Any], str] | None = None,
    ):
        self.kernel_type = kernel_type
        self.api_path = api_path
        self.description = description
        self.usage_examples = usage_examples
        self.version_help = version_help
        self.input_count = input_count
        self.fixed_kwargs = dict(fixed_kwargs or {})
        self.input_dtype_overrides = dict(input_dtype_overrides or {})
        self._prepare_override = prepare
        self._build_case_override = build_case
        self._run_case_override = run_case
        self._format_success_override = format_success

    def build_argparser(self):
        return build_standard_argparser(
            description=self.description,
            usage_examples=self.usage_examples,
            version_help=self.version_help,
        )

    def resolve_api(self):
        if not self.api_path:
            raise ValueError(f"{self.kernel_type} replay does not define api_path")

        runtime_torch, runtime_torch_npu = get_runtime_modules()
        if self.api_path.startswith("torch.ops."):
            current = runtime_torch
            parts = self.api_path.split(".")[1:]
        elif self.api_path.startswith("torch_npu."):
            current = runtime_torch_npu
            parts = self.api_path.split(".")[1:]
        elif self.api_path.startswith("torch."):
            current = runtime_torch
            parts = self.api_path.split(".")[1:]
        else:
            raise ValueError(f"Unsupported api path: {self.api_path}")

        for part in parts:
            current = getattr(current, part)
        return current

    def build_inputs(self, row: dict[str, str]) -> list[Any]:
        init_runtime()
        input_shapes = [parse_shape(item) for item in parse_list_field(row["Input Shapes"])]
        input_formats = parse_list_field(row["Input Formats"])
        input_dtypes = [
            normalize_dtype_name(item)
            for item in parse_list_field(row["Input Data Types"])
        ]

        if self.input_count is not None and len(input_shapes) != self.input_count:
            raise ValueError(
                f"{self.kernel_type} expects exactly {self.input_count} inputs, got {len(input_shapes)}"
            )

        tensors: list[Any] = []
        for index, shape in enumerate(input_shapes):
            dtype_name = self.input_dtype_overrides.get(
                index,
                input_dtypes[index] if index < len(input_dtypes) else "DT_FLOAT",
            )
            input_format = input_formats[index] if index < len(input_formats) else "ND"
            tensors.append(
                build_input_tensor(
                    shape=shape,
                    input_format=input_format,
                    dtype_name=dtype_name,
                )
            )
        return tensors

    def build_case(self, row: dict[str, str]) -> dict[str, Any]:
        if self._build_case_override is not None:
            return self._build_case_override(row)
        return {
            "inputs": self.build_inputs(row),
            "kwargs": dict(self.fixed_kwargs),
            "api": self.resolve_api() if self.api_path else None,
        }

    def run_case(self, case: dict[str, Any]) -> Any:
        if self._run_case_override is not None:
            return self._run_case_override(case)
        if case["api"] is None:
            raise ValueError(f"{self.kernel_type} replay requires api or custom run_case")
        return case["api"](*case["inputs"], **case["kwargs"])

    def synchronize(self) -> None:
        runtime_torch, _ = get_runtime_modules()
        if hasattr(runtime_torch, "npu") and runtime_torch.npu.is_available():
            runtime_torch.npu.synchronize()
        elif hasattr(runtime_torch, "cuda") and runtime_torch.cuda.is_available():
            runtime_torch.cuda.synchronize()

    def format_success(
        self,
        csv_path: str,
        row_index: int,
        row: dict[str, str],
        case: dict[str, Any],
        result: Any,
    ) -> str:
        if self._format_success_override is not None:
            return self._format_success_override(csv_path, row_index, row, case, result)

        output = result[0] if isinstance(result, tuple) and result else result
        output_shape = tuple(output.shape) if hasattr(output, "shape") else str(output)
        return (
            f"[OK] {csv_path}:{row_index} "
            f"shapes={row['Input Shapes']} formats={row['Input Formats']} "
            f"dtypes={row['Input Data Types']} output={output_shape}"
        )

    def run_row(self, csv_path, row_index: int, row: dict[str, str]) -> None:
        case = self.build_case(row)
        result = self.run_case(case)
        self.synchronize()
        print(self.format_success(csv_path, row_index, row, case, result))

    def prepare(self) -> None:
        if self._prepare_override is not None:
            self._prepare_override()

    def main(self) -> None:
        args = self.build_argparser().parse_args()
        repeat_count = get_replay_repeat_count(args.repeat_count)
        ensure_npu_available()
        self.prepare()

        target_data_dir = get_target_data_dir(
            device=args.device,
            vllm_ascend_version=args.vllm_version,
            database_path=args.database_path,
            torch_version=args.torch_version,
            cann_version=args.cann_version,
        )
        csv_name = f"{self.kernel_type}.csv"
        csv_paths = sorted(target_data_dir.rglob(csv_name))
        if not csv_paths:
            raise FileNotFoundError(f"No {csv_name} found under {target_data_dir}")

        total_rows, invalid_rows, _, skipped_rows = process_replay_csvs(
            kernel_type=self.kernel_type,
            csv_paths=csv_paths,
            repeat_count=repeat_count,
            run_row_fn=self.run_row,
            update_mode=args.update_mode,
        )

        print(
            f"Processed {total_rows} {self.kernel_type} rows from {len(csv_paths)} csv file(s) "
            f"under {target_data_dir}."
        )
        if args.update_mode == "missing-only":
            print(f"[SUMMARY] {self.kernel_type}: skipped {skipped_rows} row(s) due to missing-only mode.")
        print_invalid_replay_summary(invalid_rows, label=self.kernel_type)