from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path


@dataclass(frozen=True)
class TraceConfig:
    trace_enabled: bool = False
    debug_enabled: bool = False
    benchmark_enabled: bool = False
    lang: str = "python"
    trace_format: str = "text"
    benchmark_format: str = "text"
    trace_families: set[str] = field(default_factory=set)
    trace_signal: bool = False
    scenario: str = "single_neuron"
    value: float = 0.42
    ticks: int = 1
    out_target: str = "stdout"
    benchmark_warmup: int = 100
    benchmark_iterations: int = 1000
    benchmark_repeat: int = 5
    benchmark_output_target: str = "stdout"
    trace_neuron: str | None = None
    trace_layer: int | None = None
    config_path: str | None = None
    input_path_override: str | None = None
    input_type_override: str | None = None
    generic_output_target: str | None = None
    mode: str | None = None
    scenario_overridden: bool = False
    value_overridden: bool = False
    ticks_overridden: bool = False
    out_target_overridden: bool = False
    benchmark_output_overridden: bool = False
    trace_format_overridden: bool = False
    benchmark_format_overridden: bool = False
    mode_overridden: bool = False
    help_requested: bool = False

    @classmethod
    def parse(cls, lang: str, argv: list[str]) -> "TraceConfig":
        trace_enabled = False
        debug_enabled = False
        benchmark_enabled = False
        trace_format = "text"
        benchmark_format = "text"
        trace_families: set[str] = set()
        trace_signal = False
        scenario = "single_neuron"
        value = 0.42
        ticks = 1
        out_target = "stdout"
        benchmark_warmup = 100
        benchmark_iterations = 1000
        benchmark_repeat = 5
        benchmark_output_target = "stdout"
        trace_neuron: str | None = None
        trace_layer: int | None = None
        config_path: str | None = None
        input_path_override: str | None = None
        input_type_override: str | None = None
        generic_output_target: str | None = None
        mode: str | None = None
        scenario_overridden = False
        value_overridden = False
        ticks_overridden = False
        out_target_overridden = False
        benchmark_output_overridden = False
        trace_format_overridden = False
        benchmark_format_overridden = False
        mode_overridden = False
        help_requested = False

        arg_index = 0
        while arg_index < len(argv):
            raw_arg = argv[arg_index]
            if not raw_arg:
                arg_index += 1
                continue
            option_name = raw_arg
            option_value = None
            if "=" in raw_arg:
                option_name, option_value = raw_arg.split("=", 1)

            if option_name == "--lang":
                provided_lang = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                if cls._normalize_token(provided_lang, lang) != cls._normalize_token(lang, lang):
                    raise ValueError(f"This runner only supports --lang {lang}.")
            elif option_name == "--trace":
                trace_enabled = True
            elif option_name == "--debug":
                debug_enabled = True
            elif option_name == "--benchmark":
                benchmark_enabled = True
            elif option_name == "--trace-format":
                trace_format = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                trace_format_overridden = True
            elif option_name == "--benchmark-format":
                benchmark_format = (
                    option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                )
                if option_value is None:
                    arg_index += 1
                benchmark_format_overridden = True
            elif option_name == "--trace-events":
                raw_families = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                trace_families = {
                    cls._normalize_token(raw_part, "")
                    for raw_part in raw_families.split(",")
                    if cls._normalize_token(raw_part, "")
                }
            elif option_name == "--trace-signal":
                trace_signal = True
            elif option_name == "--scenario":
                scenario = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                scenario_overridden = True
            elif option_name == "--value":
                raw_value = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                value = float(raw_value)
                value_overridden = True
            elif option_name == "--ticks":
                raw_ticks = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                ticks = max(1, int(raw_ticks))
                ticks_overridden = True
            elif option_name == "--benchmark-warmup":
                raw_warmup = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                benchmark_warmup = max(0, int(raw_warmup))
            elif option_name == "--benchmark-iterations":
                raw_iterations = (
                    option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                )
                if option_value is None:
                    arg_index += 1
                benchmark_iterations = max(1, int(raw_iterations))
            elif option_name == "--benchmark-repeat":
                raw_repeat = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                benchmark_repeat = max(1, int(raw_repeat))
            elif option_name == "--out":
                out_target = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                out_target_overridden = True
            elif option_name == "--benchmark-output":
                benchmark_output_target = (
                    option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                )
                if option_value is None:
                    arg_index += 1
                benchmark_output_overridden = True
            elif option_name == "--trace-neuron":
                trace_neuron = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
            elif option_name == "--trace-layer":
                raw_layer = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                trace_layer = int(raw_layer)
            elif option_name == "--config":
                config_path = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
            elif option_name == "--input":
                input_path_override = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
            elif option_name == "--input-type":
                input_type_override = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
            elif option_name == "--output":
                generic_output_target = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
            elif option_name == "--mode":
                mode = option_value if option_value is not None else cls._next_value(argv, arg_index, option_name)
                if option_value is None:
                    arg_index += 1
                mode_overridden = True
                normalized_mode = cls._normalize_token(mode, "run")
                if normalized_mode == "trace":
                    trace_enabled = True
                elif normalized_mode == "benchmark":
                    benchmark_enabled = True
            elif option_name in {"--help", "-h"}:
                help_requested = True
            elif raw_arg.startswith("--"):
                raise ValueError(f"Unknown option: {raw_arg}")
            arg_index += 1

        return cls(
            trace_enabled=trace_enabled,
            debug_enabled=debug_enabled,
            benchmark_enabled=benchmark_enabled,
            lang=cls._normalize_token(lang, "python"),
            trace_format=cls._normalize_format(trace_format),
            benchmark_format=cls._normalize_format(benchmark_format),
            trace_families=trace_families,
            trace_signal=trace_signal,
            scenario=cls._normalize_token(scenario, "single_neuron"),
            value=value,
            ticks=max(1, ticks),
            out_target=out_target or "stdout",
            benchmark_warmup=max(0, benchmark_warmup),
            benchmark_iterations=max(1, benchmark_iterations),
            benchmark_repeat=max(1, benchmark_repeat),
            benchmark_output_target=benchmark_output_target or "stdout",
            trace_neuron=trace_neuron or None,
            trace_layer=trace_layer,
            config_path=config_path or None,
            input_path_override=input_path_override or None,
            input_type_override=input_type_override or None,
            generic_output_target=generic_output_target or None,
            mode=cls._normalize_token(mode, "run") if mode is not None else None,
            scenario_overridden=scenario_overridden,
            value_overridden=value_overridden,
            ticks_overridden=ticks_overridden,
            out_target_overridden=out_target_overridden,
            benchmark_output_overridden=benchmark_output_overridden,
            trace_format_overridden=trace_format_overridden,
            benchmark_format_overridden=benchmark_format_overridden,
            mode_overridden=mode_overridden,
            help_requested=help_requested,
        )

    @staticmethod
    def _normalize_token(raw_value: str | None, fallback_value: str) -> str:
        if raw_value is None or not str(raw_value).strip():
            return fallback_value
        return str(raw_value).strip().lower()

    @staticmethod
    def _next_value(argv: list[str], arg_index: int, option_name: str) -> str:
        if arg_index + 1 >= len(argv):
            raise ValueError(f"Missing value for {option_name}")
        return argv[arg_index + 1]

    @classmethod
    def _normalize_format(cls, raw_format: str | None) -> str:
        return "jsonl" if cls._normalize_token(raw_format, "text") == "jsonl" else "text"

    @property
    def writes_to_stdout(self) -> bool:
        return self.out_target.lower() == "stdout"

    @property
    def out_path(self) -> Path:
        return Path(self.out_target)

    @property
    def writes_benchmark_to_stdout(self) -> bool:
        return self.benchmark_output_target.lower() == "stdout"

    @property
    def benchmark_out_path(self) -> Path:
        return Path(self.benchmark_output_target)

    @staticmethod
    def usage(lang: str = "python") -> str:
        normalized_lang = TraceConfig._normalize_token(lang, "python")
        return "\n".join(
            [
                "Usage: grownet trace/benchmark runner",
                f"  --lang {normalized_lang}",
                "  --trace",
                "  --benchmark",
                "  --debug",
                "  --trace-format text|jsonl",
                "  --benchmark-format text|jsonl",
                "  --trace-events tick,layer,neuron,slot,weight,threshold,tract,growth,prune,bus,focus,anchor",
                "    Exact event-name filters are also accepted, for example: threshold.update",
                "  --trace-signal",
                "  --scenario single_neuron|two_layer|image_2d|growth",
                "  --value 0.42",
                "  --ticks 1",
                "  --benchmark-warmup 100",
                "  --benchmark-iterations 1000",
                "  --benchmark-repeat 5",
                "  --config path/to/experiment.json",
                "  --input path/to/input_file",
                "  --input-type sequence|tensor|image|audio",
                "  --output stdout|path/to/output.json",
                "  --mode run|trace|benchmark|interactive",
                "  --out stdout|path/to/file.log",
                "  --benchmark-output stdout|path/to/file.log",
                "  --trace-neuron E0",
                "  --trace-layer 0",
                "  --help",
            ]
        )
