from __future__ import annotations

# pyright: reportMissingImports=false

import copy
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, TypeGuard, cast

try:
    import tomllib
except ModuleNotFoundError:  # pragma: no cover - Python <3.11
    tomllib = None  # type: ignore[assignment]


@dataclass
class _FletMetadata:
    name: str | None = None
    distpath: str | None = None
    onedir: bool | None = None
    script: str | None = None
    icon: str | None = None
    product_name: str | None = None
    file_description: str | None = None
    company_name: str | None = None
    product_version: str | None = None
    bundle_id: str | None = None


@dataclass
class _DistromateConfig:
    app_name: str
    package_name: str
    package_executable: str
    package_target: str
    app_id: str | None = None
    package_icon: str | None = None
    package_description: str | None = None
    package_publisher: str | None = None


def _first_text(*values: object) -> str | None:
    for value in values:
        if isinstance(value, str):
            text = value.strip()
            if text:
                return text
        elif isinstance(value, (int, float)):
            return str(value)
    return None


def _is_mapping(value: object) -> TypeGuard[dict[str, object]]:
    return isinstance(value, dict)


def _clone_value(value: object) -> Any:
    return copy.deepcopy(value)


def _mapping_copy(value: object) -> dict[str, object]:
    if not _is_mapping(value):
        return {}
    return cast(dict[str, object], _clone_value(value))


def _deep_merge(base: object, override: object) -> object:
    if not _is_mapping(base) or not _is_mapping(override):
        return _clone_value(override)

    result = _mapping_copy(base)
    for key, value in override.items():
        if value is None:
            result[key] = None
            continue
        if _is_mapping(value) and _is_mapping(result.get(key)):
            result[key] = _deep_merge(result[key], value)
        else:
            result[key] = _clone_value(value)
    return result


def _strip_none(value: object) -> object:
    if isinstance(value, list):
        return [_strip_none(item) for item in value]
    if _is_mapping(value):
        result: dict[str, object] = {}
        for key, child in value.items():
            if child is not None:
                result[key] = _strip_none(child)
        return result
    return value


def _normalize_overlay_aliases(config: object) -> dict[str, object]:
    if not _is_mapping(config):
        return {}

    next_config = _mapping_copy(config)
    next_config.pop("name", None)
    next_config.pop("distribution", None)
    next_config.pop("cloudAppId", None)
    next_config.pop("appid", None)
    return next_config


def _read_pyproject_version(start_dir: Path) -> str | None:
    if tomllib is None:
        return None

    for current in [start_dir, *start_dir.parents]:
        pyproject = current / "pyproject.toml"
        if not pyproject.is_file():
            continue
        try:
            data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
        except Exception:
            return None
        return data.get("project", {}).get("version")

    return None


def _normalize_yaml_path(path_value: str, start_dir: Path) -> str:
    path = Path(path_value)
    if path.is_absolute():
        try:
            return path.relative_to(start_dir).as_posix()
        except ValueError:
            return path.as_posix()
    return path.as_posix()


def _normalize_icon_value(icon_value: str | None, start_dir: Path) -> str | None:
    if icon_value is None:
        return None

    normalized = icon_value.strip()
    if not normalized or normalized.lower() == "none":
        return None

    return _normalize_yaml_path(normalized, start_dir)


def _join_yaml_path(*parts: str) -> str:
    normalized: list[str] = []
    for idx, part in enumerate(parts):
        current = part.replace("\\", "/")
        if idx == 0:
            normalized.append(current.rstrip("/"))
        else:
            normalized.append(current.strip("/"))
    merged = "/".join(item for item in normalized if item)
    return merged or "."


def _parse_flet_metadata(flet_args: list[str]) -> _FletMetadata:
    metadata = _FletMetadata()
    i = 0
    while i < len(flet_args):
        arg = str(flet_args[i]).strip()
        if not arg:
            i += 1
            continue

        if arg in {"-n", "--name"} and i + 1 < len(flet_args):
            metadata.name = str(flet_args[i + 1]).strip() or metadata.name
            i += 2
            continue
        if arg.startswith("--name="):
            metadata.name = arg.split("=", 1)[1].strip() or metadata.name
            i += 1
            continue

        if arg in {"-i", "--icon"} and i + 1 < len(flet_args):
            metadata.icon = str(flet_args[i + 1]).strip() or metadata.icon
            i += 2
            continue
        if arg.startswith("--icon="):
            metadata.icon = arg.split("=", 1)[1].strip() or metadata.icon
            i += 1
            continue
        if arg.startswith("-i") and len(arg) > 2:
            metadata.icon = arg[2:].strip() or metadata.icon
            i += 1
            continue

        if arg == "--distpath" and i + 1 < len(flet_args):
            metadata.distpath = str(flet_args[i + 1]).strip() or metadata.distpath
            i += 2
            continue
        if arg.startswith("--distpath="):
            metadata.distpath = arg.split("=", 1)[1].strip() or metadata.distpath
            i += 1
            continue

        if arg in {"-D", "--onedir"}:
            metadata.onedir = True
            i += 1
            continue

        if arg == "--product-name" and i + 1 < len(flet_args):
            metadata.product_name = str(flet_args[i + 1]).strip() or metadata.product_name
            i += 2
            continue
        if arg.startswith("--product-name="):
            metadata.product_name = arg.split("=", 1)[1].strip() or metadata.product_name
            i += 1
            continue

        if arg == "--file-description" and i + 1 < len(flet_args):
            metadata.file_description = str(flet_args[i + 1]).strip() or metadata.file_description
            i += 2
            continue
        if arg.startswith("--file-description="):
            metadata.file_description = arg.split("=", 1)[1].strip() or metadata.file_description
            i += 1
            continue

        if arg == "--company-name" and i + 1 < len(flet_args):
            metadata.company_name = str(flet_args[i + 1]).strip() or metadata.company_name
            i += 2
            continue
        if arg.startswith("--company-name="):
            metadata.company_name = arg.split("=", 1)[1].strip() or metadata.company_name
            i += 1
            continue

        if arg == "--product-version" and i + 1 < len(flet_args):
            metadata.product_version = str(flet_args[i + 1]).strip() or metadata.product_version
            i += 2
            continue
        if arg.startswith("--product-version="):
            metadata.product_version = arg.split("=", 1)[1].strip() or metadata.product_version
            i += 1
            continue

        if arg == "--bundle-id" and i + 1 < len(flet_args):
            metadata.bundle_id = str(flet_args[i + 1]).strip() or metadata.bundle_id
            i += 2
            continue
        if arg.startswith("--bundle-id="):
            metadata.bundle_id = arg.split("=", 1)[1].strip() or metadata.bundle_id
            i += 1
            continue

        if not arg.startswith("-") and metadata.script is None:
            metadata.script = arg

        i += 1

    return metadata


def _derive_executable_path(package_target: str, package_name: str, onedir: bool) -> str:
    if sys.platform == "darwin":
        return _join_yaml_path(package_target, f"{package_name}.app", "Contents", "MacOS", package_name)

    executable_name = package_name
    if os.name == "nt" and not executable_name.lower().endswith(".exe"):
        executable_name += ".exe"

    if onedir:
        return _join_yaml_path(package_target, package_name, executable_name)
    return _join_yaml_path(package_target, executable_name)


def _derive_distromate_config(flet_args: list[str], start_dir: Path, app_id: str | None = None) -> _DistromateConfig:
    metadata = _parse_flet_metadata(flet_args)

    raw_name = metadata.name
    if not raw_name and metadata.script:
        raw_name = Path(metadata.script).stem
    if not raw_name:
        raw_name = start_dir.name or "app"

    package_name = Path(raw_name).name.strip() or "app"
    if package_name.lower().endswith(".exe") and len(package_name) > 4:
        package_name = package_name[:-4]

    package_target = _normalize_yaml_path(metadata.distpath or "dist", start_dir)
    package_icon = _normalize_icon_value(metadata.icon, start_dir)
    package_description = _first_text(metadata.file_description)
    package_publisher = _first_text(metadata.company_name)
    resolved_app_id = _first_text(app_id, metadata.bundle_id)
    app_name = _first_text(metadata.product_name, package_name) or package_name
    onedir = metadata.onedir is True

    return _DistromateConfig(
        app_id=resolved_app_id,
        app_name=app_name,
        package_name=package_name,
        package_executable=_derive_executable_path(package_target, package_name, onedir),
        package_target=package_target,
        package_icon=package_icon,
        package_description=package_description,
        package_publisher=package_publisher,
    )


def _default_package_files() -> list[str]:
    return ["**/*", "!**/.git/**", "!**/*.map", "!**/*.pdb"]


def _build_distromate_project_config(
    start_dir: Path,
    flet_args: list[str],
    app_id: str | None = None,
    base_context: dict[str, object] | None = None,
) -> dict[str, object]:
    derived = _derive_distromate_config(flet_args, start_dir, app_id=app_id)
    overlay = _normalize_overlay_aliases(base_context)
    merged = _mapping_copy(
        _deep_merge(
            {
                "productName": derived.app_name,
                "publish": {"appId": app_id} if app_id else {},
                "source": {
                    "type": "native",
                    "root": derived.package_target,
                    "executable": derived.package_executable,
                    "files": _default_package_files(),
                },
                "package": {
                    "name": derived.package_name,
                    "shortDescription": derived.package_description,
                    "icon": derived.package_icon,
                    "publisher": derived.package_publisher,
                },
            },
            overlay,
        )
    )

    source = _mapping_copy(merged.get("source"))
    package = _mapping_copy(merged.get("package"))
    publish = _mapping_copy(merged.get("publish"))

    system_app_id = _first_text(merged.get("appId"), derived.app_id)
    publish_app_id = _first_text(app_id, publish.get("appId"), derived.app_id)

    product_name = _first_text(merged.get("productName"), derived.app_name)
    package_name = _first_text(package.get("name"), derived.package_name)
    package_description = _first_text(package.get("shortDescription"), derived.package_description)
    package_icon = _first_text(package.get("icon"), derived.package_icon)
    package_publisher = _first_text(package.get("publisher"), derived.package_publisher)

    if product_name:
        merged["productName"] = product_name
    if package_name:
        package["name"] = package_name
    if package_description:
        package["shortDescription"] = package_description
    if package_icon:
        package["icon"] = package_icon
    if package_publisher:
        package["publisher"] = package_publisher

    source["type"] = "native"
    source["root"] = derived.package_target
    source["executable"] = derived.package_executable
    source.pop("plugin", None)
    source.pop("options", None)
    if not source.get("files"):
        source["files"] = _default_package_files()

    package.pop("target", None)
    package.pop("executable", None)
    package.pop("files", None)
    package.pop("args", None)
    package.pop("executableName", None)

    if system_app_id:
        merged["appId"] = system_app_id
        package["appId"] = _first_text(package.get("appId"), system_app_id)

    if publish_app_id:
        publish["appId"] = publish_app_id
    if publish:
        merged["publish"] = publish
    else:
        merged.pop("publish", None)

    top_level_icon = _first_text(merged.get("icon"), package_icon)
    if top_level_icon:
        merged["icon"] = top_level_icon

    merged["source"] = source
    merged["package"] = package
    merged.pop("distromateCliPath", None)
    merged.pop("cliPath", None)

    cleaned = _strip_none(merged)
    if _is_mapping(cleaned):
        return cleaned
    return {}
