#!/usr/bin/env python3
"""Inspect Core ML operation placement and estimated accelerator cost share."""

from __future__ import annotations

import argparse
import platform
import sys
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any, Sequence

from purpose_data import DataError, write_json


def _device_category(device: Any) -> str:
    name = type(device).__name__.lower()
    description = str(device).lower()
    combined = f"{name} {description}"
    if "neural" in combined:
        return "neuralEngine"
    if "gpu" in combined:
        return "gpu"
    if "cpu" in combined:
        return "cpu"
    return "unknown"


def _compute_unit(coremltools: Any, requested: str) -> Any:
    values = {
        "all": coremltools.ComputeUnit.ALL,
        "cpu-only": coremltools.ComputeUnit.CPU_ONLY,
        "cpu-and-gpu": coremltools.ComputeUnit.CPU_AND_GPU,
        "cpu-and-ne": coremltools.ComputeUnit.CPU_AND_NE,
    }
    return values[requested]


def inspect(args: argparse.Namespace) -> dict[str, Any]:
    if not args.model.exists():
        raise DataError(f"{args.model}: Core ML model is missing")
    try:
        import coremltools as ct
    except ImportError as exc:
        raise DataError(
            "Core ML inspection requires requirements-coreml.txt on macOS"
        ) from exc

    compiled = ct.models.utils.compile_model(str(args.model))
    compute_plan = ct.models.compute_plan.MLComputePlan.load_from_path(
        path=str(compiled),
        compute_units=_compute_unit(ct, args.compute_units),
    )
    program = compute_plan.model_structure.program
    if program is None or "main" not in program.functions:
        raise DataError("Core ML package is not an ML Program with a main function")
    operations = list(program.functions["main"].block.operations)
    if not operations:
        raise DataError("Core ML compute plan contains no operations")

    preferred_counts: Counter[str] = Counter()
    preferred_costs: dict[str, float] = defaultdict(float)
    supported_counts: Counter[str] = Counter()
    operation_reports = []
    operations_with_usage = 0
    operations_with_cost = 0
    total_cost = 0.0
    for operation in operations:
        usage = compute_plan.get_compute_device_usage_for_mlprogram_operation(
            operation
        )
        cost = compute_plan.get_estimated_cost_for_mlprogram_operation(operation)
        preferred = "unknown"
        supported: list[str] = []
        if usage is not None:
            operations_with_usage += 1
            preferred = _device_category(usage.preferred_compute_device)
            preferred_counts[preferred] += 1
            supported = sorted(
                {_device_category(device) for device in usage.supported_compute_devices}
            )
            supported_counts.update(supported)
        weight = None
        if cost is not None:
            operations_with_cost += 1
            weight = float(cost.weight)
            total_cost += weight
            preferred_costs[preferred] += weight
        operation_reports.append(
            {
                "operatorName": str(operation.operator_name),
                "preferredDevice": preferred,
                "supportedDevices": supported,
                "estimatedCostWeight": weight,
            }
        )

    ane_operations = preferred_counts["neuralEngine"]
    ane_cost = preferred_costs["neuralEngine"]
    report = {
        "schemaVersion": 1,
        "model": str(args.model),
        "coremltoolsVersion": ct.__version__,
        "machine": platform.machine(),
        "macOS": platform.mac_ver()[0],
        "computeUnits": args.compute_units,
        "operations": len(operations),
        "operationsWithDeviceUsage": operations_with_usage,
        "operationsWithEstimatedCost": operations_with_cost,
        "preferredOperationCounts": dict(sorted(preferred_counts.items())),
        "supportedOperationCounts": dict(sorted(supported_counts.items())),
        "preferredEstimatedCosts": dict(sorted(preferred_costs.items())),
        "neuralEngineOperationShare": (
            ane_operations / operations_with_usage if operations_with_usage else 0.0
        ),
        "neuralEngineEstimatedCostShare": (
            ane_cost / total_cost if total_cost else 0.0
        ),
        "operationDetails": operation_reports,
    }
    write_json(args.report, report)
    print(
        "Core ML placement: "
        f"ANE operations={report['neuralEngineOperationShare']:.2%} "
        f"ANE estimated cost={report['neuralEngineEstimatedCostShare']:.2%}"
    )
    return report


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--model", type=Path, required=True)
    parser.add_argument("--report", type=Path, required=True)
    parser.add_argument(
        "--compute-units",
        choices=("all", "cpu-only", "cpu-and-gpu", "cpu-and-ne"),
        default="cpu-and-ne",
    )
    return parser


def main(argv: Sequence[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    try:
        inspect(args)
    except (DataError, OSError, RuntimeError, ValueError) as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
