from __future__ import annotations

import re
from collections.abc import Callable
from collections.abc import Sized
from pathlib import Path
from typing import cast

from loguru import logger

from .. import _yaml
from ._writer import set_override

here = Path(__file__).resolve().parent


def _update_dict(
    target_dict: dict[str, object],
    new_dict: dict[str, object],
    validate_item: Callable[[str, object], None] | None = None,
) -> None:
    for key, value in new_dict.items():
        if validate_item:
            validate_item(key, value)
        if key not in target_dict:
            raise ValueError(f"Unexpected key in config: {key}")
        if not isinstance(target_dict[key], dict):
            target_dict[key] = value
            continue

        # target_dict[key] is a section, so the override must be a mapping.
        if value is None:
            # An empty section (e.g. a bare `shortcuts:`) keeps its defaults
            # instead of wiping the whole section.
            continue
        if not isinstance(value, dict):
            # A non-mapping override (e.g. `shortcuts: oops`) would wipe the
            # section with a scalar and crash the app downstream; surface it as
            # a config error instead.
            raise ValueError(
                f"Config section {key!r} must be a mapping, "
                f"but got {type(value).__name__}: {value!r}"
            )
        _update_dict(
            cast(dict[str, object], target_dict[key]),
            cast(dict[str, object], value),
            validate_item=validate_item,
        )


def _validate_config_item(key: str, value: object) -> None:
    if key == "validate_label" and value not in [None, "exact"]:
        raise ValueError(f"Unexpected value for config key 'validate_label': {value}")
    if key == "shape_color" and value not in [None, "auto", "manual"]:
        raise ValueError(f"Unexpected value for config key 'shape_color': {value}")
    if (
        key == "labels"
        and isinstance(value, Sized)
        and len(value) != len(set(cast(list[object], value)))
    ):
        raise ValueError(f"Duplicates are detected for config key 'labels': {value}")


def _migrate_config_from_file(config_from_yaml: dict) -> None:
    keep_prev_brightness: bool = config_from_yaml.pop("keep_prev_brightness", False)
    keep_prev_contrast: bool = config_from_yaml.pop("keep_prev_contrast", False)
    if keep_prev_brightness or keep_prev_contrast:
        logger.info(
            "Migrating old config: keep_prev_brightness={} or keep_prev_contrast={} "
            "-> keep_prev_brightness_contrast=True",
            keep_prev_brightness,
            keep_prev_contrast,
        )
        config_from_yaml["keep_prev_brightness_contrast"] = True

    if "store_data" in config_from_yaml:
        logger.info("Migrating old config: store_data -> with_image_data")
        config_from_yaml["with_image_data"] = config_from_yaml.pop("store_data")

    # A malformed section (e.g. `shortcuts: oops`) is left untouched here so the
    # merge in _update_dict reports it as a config error instead of crashing.
    shortcuts = config_from_yaml.get("shortcuts")
    if not isinstance(shortcuts, dict):
        shortcuts = {}
    if shortcuts.pop("add_point_to_edge", None):
        logger.info("Migrating old config: removing shortcuts.add_point_to_edge")

    ai = config_from_yaml.get("ai")
    if (
        isinstance(ai, dict)
        and (model_name := ai.get("default"))
        and (m := re.match(r"^SegmentAnything \((.*)\)$", model_name))
    ):
        model_name_new: str = f"Sam ({m.group(1)})"
        logger.info(
            "Migrating old config: ai.default={!r} -> ai.default={!r}",
            model_name,
            model_name_new,
        )
        ai["default"] = model_name_new

    # Migrate polygon shortcut keys to shape
    _POLYGON_TO_SHAPE_RENAMES = {
        "edit_polygon": "edit_shape",
        "delete_polygon": "delete_shape",
        "duplicate_polygon": "duplicate_shape",
        "copy_polygon": "copy_shape",
        "paste_polygon": "paste_shape",
        "show_all_polygons": "show_all_shapes",
        "hide_all_polygons": "hide_all_shapes",
        "toggle_all_polygons": "toggle_all_shapes",
    }
    for old_key, new_key in _POLYGON_TO_SHAPE_RENAMES.items():
        if old_key in shortcuts and new_key not in shortcuts:
            logger.info(
                "Migrating old config: shortcuts.{} -> shortcuts.{}",
                old_key,
                new_key,
            )
            shortcuts[new_key] = shortcuts.pop(old_key)


def get_user_config_file(create_if_missing: bool = True) -> str:
    user_config_path = Path("~/.labelmerc").expanduser()
    if not user_config_path.exists() and create_if_missing:
        try:
            user_config_path.touch()
        except Exception:
            logger.warning("Failed to save config: {!r}", str(user_config_path))
    return str(user_config_path)


def load_config(config_file: Path | None, config_overrides: dict) -> dict:
    config: dict
    with open(here / "default_config.yaml", encoding="utf-8") as f:
        config = _yaml.safe_load(f)

    if config_file is not None:
        with open(config_file, encoding="utf-8") as f:
            config_from_yaml = _yaml.safe_load(f)
        if isinstance(config_from_yaml, dict):
            _migrate_config_from_file(config_from_yaml=config_from_yaml)
            _update_dict(config, config_from_yaml, validate_item=_validate_config_item)

    _update_dict(config, config_overrides, validate_item=_validate_config_item)

    if not config["labels"] and config["validate_label"]:
        raise ValueError("labels must be specified when validate_label is enabled")

    return config
