import re
import warnings
from pathlib import Path
from typing import Any, Dict, List, Optional, Type

import yaml

from bl.types import OriginType, ProjectSpec, RefspecInfo, RepoInfo


def make_remote_merge_from_src(src: str) -> tuple[dict, list]:
    """
    Creates a remote and merge entry from the src string.
    """
    # TODO(franz): if there is 3 part:
    # - url: parts[0]
    # - branch: parts[1]
    # - sha: parts[2]
    remotes = {}
    merges = []

    parts = src.split(" ", 1)
    remotes["origin"] = parts[0]
    merges.append(f"origin {parts[1]}")

    return remotes, merges


def get_origin_type(origin_value: str) -> OriginType:
    """
    Determines the origin type based on the origin value.

    Args:
        origin_value: The origin string to evaluate.

    Returns:
        The corresponding OriginType.
    """
    # Pattern to match GitHub PR references: refs/pull/{pr_id}/head
    pr_pattern = re.compile(r"^refs/pull/\d+/head$")
    # Pattern to match that matches git reference hashes (40 hex characters)
    ref_pattern = re.compile(r"^[a-z0-9]{40}$")

    if pr_pattern.match(origin_value):
        return OriginType.PR
    elif ref_pattern.match(origin_value):
        return OriginType.REF
    else:
        return OriginType.BRANCH


def parse_remote_refspec_from_entry(entry: List[str] | Dict[str, str], frozen_repo: Dict[str, Dict[str, str]]):
    if isinstance(entry, list):
        if len(entry) == 2:
            entry.insert(1, "")
        else:
            warnings.warn(
                "Deprecated src format: use <url> <sha> format for the src property",
                DeprecationWarning,
            )
        remote_key, _, ref_spec = entry
    else:
        remote_key = entry.get("remote")
        # Here 16.0 can be parsed as float, todo: change spec to a sane file format
        ref_spec = str(entry.get("ref"))

    ref_type = get_origin_type(ref_spec)

    ref_name = None
    remote_freezes = frozen_repo.get(remote_key, {})

    if ref_spec in remote_freezes:
        ref_type = OriginType.REF
        ref_name = ref_spec
        ref_spec = remote_freezes.get(ref_name)

    return RefspecInfo(remote_key, ref_spec, ref_type, ref_name)


def get_with_syntax_check(name, data, key: str, type: Type):
    result = data.get(key, type())
    if not isinstance(result, type):
        raise Exception(f"Key {key} not of proper syntax should be {str(type)} in {name} description")
    return result


def merge_configs(base: Dict[str, Any], override: Dict[str, Any]) -> None:
    """
    Recursively merges the override configuration into the base configuration.

    Args:
        base: The original configuration dictionary to be modified.
        override: The configuration dictionary with override values.

    This function modifies the base dictionary in place, merging values from the override dictionary.
    If a key exists in both dictionaries and both values are dictionaries, they will be merged recursively.
    Otherwise, the value from the override dictionary will replace the value in the base dictionary.
    """
    for key, override_value in override.items():
        if key in base:
            base_value = base[key]
            if isinstance(base_value, dict) and isinstance(override_value, dict):
                merge_configs(base_value, override_value)
            elif isinstance(base_value, list) and isinstance(override_value, list):
                if "..." in override_value:
                    extend_idx = override_value.index("...")
                    base[key] = override_value[:extend_idx] + base_value + override_value[extend_idx + 1 :]
                else:
                    base[key] = override_value
            else:
                base[key] = override_value
        else:
            base[key] = override_value


def load_spec_file(
    config: Path, frozen: Path, workdir: Path, overrides: list[Path] | None = None
) -> Optional[ProjectSpec]:
    """
    Loads and parses the project specification from a YAML file.

    Reads the main config YAML, applies any override files on top of it,
    loads frozen commit references, and constructs a ProjectSpec containing
    all parsed repository definitions.

    If the config file is not found at the given path and is relative,
    a fallback lookup inside an ``odoo/`` subdirectory is attempted.

    Args:
        config: Path to the main YAML specification file.
        frozen: Path to the frozen YAML file that pins specific commits.
            Falls back to a ``frozen.yaml`` next to the config file when not provided.
        workdir: Working directory for the project. Falls back to the
            config file's parent directory when not provided.
        overrides: Optional list of paths to override YAML files that are
            merged on top of the base configuration in order.

    Returns:
        A ProjectSpec object if successful, None otherwise.
    """
    if not config.exists():
        if config.is_relative_to("."):
            config = config.resolve()
            # If the file is not in the current directory, check inside the odoo subdirectory
            odoo_config = config.parent / "odoo" / config.name
            # TODO(franz): should use rich console for prettiness
            if not odoo_config.exists():
                print(f"Error: Neither '{config}' nor '{odoo_config}' exists.")
                return None
            config = odoo_config
        else:
            print(f"Error: File '{config}' does not exist.")
            return None

    workdir = workdir or config.parent

    with config.open("r") as f:
        try:
            data: Dict[str, Any] = yaml.safe_load(f)
        except yaml.YAMLError as e:
            print(f"Error parsing YAML file '{config}': {e}")
            return None

    for override in overrides or []:
        if not override.exists():
            print(f"Override file '{override}' does not exist. Skipping.")
            continue
        try:
            with override.open("r") as override_file:
                override_data = yaml.safe_load(override_file) or {}
                if isinstance(override_data, dict):
                    merge_configs(data, override_data)
        except yaml.YAMLError as e:
            print(f"Error parsing override YAML file '{override}': {e}")

    frozen_mapping: Dict[str, Dict[str, Dict[str, str]]] = {}
    frozen_path = frozen or Path(config).with_name("frozen.yaml")
    if frozen_path.exists():
        try:
            with frozen_path.open("r") as frozen_file:
                loaded_freezes = yaml.safe_load(frozen_file) or {}
                if isinstance(loaded_freezes, dict):
                    frozen_mapping = loaded_freezes
        except yaml.YAMLError as e:
            print(f"Error parsing frozen YAML file '{frozen_path}': {e}")

    repos: Dict[str, RepoInfo] = {}
    for repo_name, repo_data in data.items():
        modules = get_with_syntax_check(repo_name, repo_data, "modules", list)
        src = get_with_syntax_check(repo_name, repo_data, "src", str)
        remotes = get_with_syntax_check(repo_name, repo_data, "remotes", dict)
        merges = get_with_syntax_check(repo_name, repo_data, "merges", list)
        paths = get_with_syntax_check(repo_name, repo_data, "paths", dict)
        shell_commands = get_with_syntax_check(repo_name, repo_data, "shell_command_after", list)
        patch_globs_to_apply = get_with_syntax_check(repo_name, repo_data, "patch_globs", list)
        target_folder = get_with_syntax_check(repo_name, repo_data, "target_folder", str)
        locales = get_with_syntax_check(repo_name, repo_data, "locales", list)

        frozen_repo = frozen_mapping.get(repo_name, {})

        # Parse merges into RefspecInfo objects
        refspec_infos: List[RefspecInfo] = []
        if src:
            # If src is defined, create a remote and merge entry from it
            src_remotes, src_merges = make_remote_merge_from_src(src)
            remotes.update(src_remotes)
            merges = src_merges + merges

        for merge_entry in merges:
            if isinstance(merge_entry, str):
                merge_entry = merge_entry.split(" ", 2)

            refspec_info = parse_remote_refspec_from_entry(merge_entry, frozen_repo)
            refspec_infos.append(refspec_info)

        repos[repo_name] = RepoInfo(
            modules,
            remotes,
            refspec_infos,
            shell_commands,
            patch_globs_to_apply,
            target_folder,
            locales,
            paths,
        )

    return ProjectSpec(repos, workdir)
