#!/usr/bin/env -S uv run python
# SPDX-License-Identifier: MIT
# Copyright (c) 2018 Interop Tokyo ShowNet NOC team
# Copyright (c) 2026 deadmon contributors
# Based on the original deadman work by upa@haeena.net.

from __future__ import annotations

import argparse
import sys
from pathlib import Path
from typing import Any

import yaml

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from deadmon.app import (  # noqa: E402
    AlertChannel,
    AlertOverride,
    ConfigError,
    DeadmonConfig,
    GroupConfig,
    TargetConfig,
    normalize_config,
    parse_config_text,
)


class IndentedSafeDumper(yaml.SafeDumper):
    def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
        return super().increase_indent(flow=flow, indentless=False)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        prog="deadmon-convert-config",
        description="Normalize JSON or YAML deadmon configs to canonical YAML.",
    )
    parser.add_argument("config", help="input config path, or '-' for stdin")
    parser.add_argument(
        "-o",
        "--output",
        default="-",
        help="output YAML path, or '-' for stdout. Defaults to stdout.",
    )
    parser.add_argument(
        "-f",
        "--force",
        action="store_true",
        help="overwrite the output file if it already exists",
    )
    parser.add_argument(
        "--no-header",
        action="store_true",
        help="omit the generated-file comment header",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    source_label = "stdin" if args.config == "-" else args.config

    try:
        text = read_input(args.config)
        raw_config = parse_config_text(text)
        config_path = Path(source_label)
        config = normalize_config(raw_config, config_path)
        yaml_text = dump_yaml(config, source_label=source_label, include_header=not args.no_header)
        write_output(args.output, yaml_text, force=args.force)
    except (ConfigError, OSError, ValueError, yaml.YAMLError) as exc:
        print(f"deadmon-convert-config: {exc}", file=sys.stderr)
        return 2

    return 0


def read_input(path: str) -> str:
    if path == "-":
        return sys.stdin.read()
    return Path(path).read_text(encoding="utf-8")


def write_output(path: str, text: str, force: bool) -> None:
    if path == "-":
        print(text, end="")
        return

    output_path = Path(path)
    if output_path.exists() and not force:
        raise ValueError(f"output already exists: {output_path} (use --force to overwrite)")
    output_path.write_text(text, encoding="utf-8")


def dump_yaml(config: DeadmonConfig, source_label: str, include_header: bool) -> str:
    data = config_to_yaml_data(config)
    body = yaml.dump(
        data,
        Dumper=IndentedSafeDumper,
        default_flow_style=False,
        sort_keys=False,
        width=100,
    )
    if not include_header:
        return body
    return (
        "# Generated by deadmon-convert-config.\n"
        f"# Source: {source_label}\n"
        "# Review group names and alert channels before deployment.\n"
        f"{body}"
    )


def config_to_yaml_data(config: DeadmonConfig) -> dict[str, Any]:
    return {
        "app": {
            "name": config.name,
            **optional_item("public_url", config.public_url),
            "poll_interval": clean_number(config.poll_interval),
            "tab_rotation_interval": clean_number(config.tab_rotation_interval),
            "timeout": clean_number(config.timeout),
            "rtt_scale_ms": config.rtt_scale_ms,
            "latency_warning_ms": clean_number(config.latency_warning_ms),
            "latency_critical_ms": clean_number(config.latency_critical_ms),
            "retain_results": config.retain_results,
        },
        "alerts": {
            "enabled": config.alerts.enabled,
            "threshold": config.alerts.threshold,
            "clear_threshold": config.alerts.clear_threshold,
            "channels": [alert_channel_to_dict(channel) for channel in config.alerts.channels],
        },
        "groups": [group_to_dict(group) for group in config.groups],
    }


def alert_channel_to_dict(channel: AlertChannel) -> dict[str, Any]:
    data: dict[str, Any] = {
        "name": channel.name,
        "type": channel.kind,
        "enabled": channel.enabled,
    }
    if channel.webhook_url_env:
        data["webhook_url_env"] = channel.webhook_url_env
    if channel.webhook_url:
        data["webhook_url"] = channel.webhook_url
    if channel.destination_channel:
        data["channel"] = channel.destination_channel
    if channel.icon_emoji:
        data["icon_emoji"] = channel.icon_emoji
    if channel.timeout != 5.0:
        data["timeout"] = clean_number(channel.timeout)
    return data


def group_to_dict(group: GroupConfig) -> dict[str, Any]:
    data: dict[str, Any] = {"name": group.name}
    if group.description:
        data["description"] = group.description
    if group.latency_warning_ms is not None:
        data["latency_warning_ms"] = clean_number(group.latency_warning_ms)
    if group.latency_critical_ms is not None:
        data["latency_critical_ms"] = clean_number(group.latency_critical_ms)
    if group.alerts:
        data["alerts"] = alert_override_to_dict(group.alerts)
    data["targets"] = [target_to_dict(target) for target in group.targets]
    return data


def target_to_dict(target: TargetConfig) -> dict[str, Any]:
    data: dict[str, Any] = {
        "name": target.name,
        "address": target.address,
    }
    if target.note:
        data["note"] = target.note
    if target.info_url:
        data["info_url"] = target.info_url
    if target.source:
        data["source"] = target.source
    if target.relay:
        data["relay"] = clean_mapping(target.relay)
    elif target.osname:
        data["osname"] = target.osname
    if target.tcp:
        data["tcp"] = clean_tcp(target.tcp)
    if target.alerts is not None:
        data["alerts"] = target.alerts
    if target.latency_warning_ms is not None:
        data["latency_warning_ms"] = clean_number(target.latency_warning_ms)
    if target.latency_critical_ms is not None:
        data["latency_critical_ms"] = clean_number(target.latency_critical_ms)
    return data


def alert_override_to_dict(alerts: AlertOverride) -> dict[str, Any]:
    data: dict[str, Any] = {}
    if alerts.enabled is not None:
        data["enabled"] = alerts.enabled
    if alerts.threshold is not None:
        data["threshold"] = alerts.threshold
    if alerts.clear_threshold is not None:
        data["clear_threshold"] = alerts.clear_threshold
    if alerts.channels is not None:
        data["channels"] = [alert_channel_to_dict(channel) for channel in alerts.channels]
    return data


def clean_mapping(mapping: dict[str, Any]) -> dict[str, Any]:
    return {str(key): clean_scalar(value) for key, value in mapping.items()}


def optional_item(key: str, value: Any) -> dict[str, Any]:
    if value is None or value == "":
        return {}
    return {key: value}


def clean_tcp(mapping: dict[str, Any]) -> dict[str, Any]:
    data = clean_mapping(mapping)
    if isinstance(data.get("dstport"), str) and data["dstport"].isdigit():
        data["dstport"] = int(data["dstport"])
    return data


def clean_scalar(value: Any) -> Any:
    if isinstance(value, dict):
        return clean_mapping(value)
    if isinstance(value, list):
        return [clean_scalar(item) for item in value]
    if isinstance(value, float):
        return clean_number(value)
    return value


def clean_number(value: float) -> float | int:
    if isinstance(value, float) and value.is_integer():
        return int(value)
    return value


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