from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path
from typing import Any

from . import project

PLUGIN_PROTOCOL = "distromate.plugin"
PLUGIN_API_VERSION = 3
FLET_CLI_BOOTSTRAP = "import sys, flet.cli; sys.argv = ['flet', *sys.argv[1:]]; flet.cli.main()"
PLUGIN_MANIFEST = {
    "apiVersion": PLUGIN_API_VERSION,
    "kind": "builder",
    "name": "flet",
    "framework": "flet",
    "runtime": "python",
}


def _read_request() -> dict[str, Any]:
    text = sys.stdin.read().strip()
    if not text:
        raise ValueError("plugin stdio request is empty")
    loaded = json.loads(text)
    if not isinstance(loaded, dict):
        raise ValueError("plugin stdio request must be a JSON object")
    return loaded


def _validate_request(request: dict[str, Any]) -> tuple[str, dict[str, Any]]:
    protocol = str(request.get("protocol") or "").strip()
    if protocol != PLUGIN_PROTOCOL:
        raise ValueError(f"plugin stdio request protocol must be {PLUGIN_PROTOCOL}")

    api_version = request.get("version")
    if api_version != PLUGIN_API_VERSION:
        raise ValueError(f"plugin stdio request version must be {PLUGIN_API_VERSION}")

    command = str(request.get("command") or "").strip()
    if command not in {"manifest", "prepare-package", "prepare-publish"}:
        raise ValueError(f"Unsupported plugin stdio command: {command or '<empty>'}")

    raw_input = request.get("input") or {}
    if not isinstance(raw_input, dict):
        raise ValueError("plugin stdio request input must be an object")
    return command, raw_input


def _source_options(input_data: dict[str, Any]) -> dict[str, Any]:
    source_options = input_data.get("options")
    if not isinstance(source_options, dict):
        return {}
    return dict(source_options)


def _context(input_data: dict[str, Any]) -> dict[str, Any]:
    context = input_data.get("config")
    if not isinstance(context, dict):
        return {}
    return dict(context)


def _first_text(*values: Any) -> 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 _bool_value(value: Any) -> bool | None:
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        text = value.strip().lower()
        if text in {"true", "1", "yes", "on"}:
            return True
        if text in {"false", "0", "no", "off"}:
            return False
    return None


def _normalize_string_list(value: Any, label: str) -> list[str]:
    if value is None:
        return []
    if isinstance(value, str):
        text = value.strip()
        return [text] if text else []
    if not isinstance(value, list):
        raise ValueError(f"{label} must be a string or an array")
    result: list[str] = []
    for index, item in enumerate(value):
        text = _first_text(item)
        if not text:
            raise ValueError(f"{label}[{index}] must be a non-empty string")
        result.append(text)
    return result


def _append_repeated_flag(target: list[str], flag: str, items: Any, label: str) -> None:
    for item in _normalize_string_list(items, label):
        target.extend([flag, item])


def _resolve_flet_args(options: dict[str, Any]) -> list[str]:
    args = _normalize_string_list(
        options.get("fletArgs", options.get("args", options.get("packArgs"))),
        "source.options.fletArgs",
    )
    if args:
        return _ensure_noninteractive_args(args)

    generated: list[str] = []
    if _bool_value(options.get("onedir")) is True:
        generated.append("--onedir")
    if _bool_value(options.get("yes")) is True:
        generated.append("--yes")
    if _bool_value(options.get("uacAdmin")) is True:
        generated.append("--uac-admin")
    if _bool_value(options.get("debugConsole")) is True:
        generated.append("--debug-console")
    if _bool_value(options.get("verbose")) is True:
        generated.append("--verbose")

    for option_key, flag in (
        ("name", "--name"),
        ("distpath", "--distpath"),
        ("icon", "--icon"),
        ("productName", "--product-name"),
        ("fileDescription", "--file-description"),
        ("productVersion", "--product-version"),
        ("fileVersion", "--file-version"),
        ("companyName", "--company-name"),
        ("copyright", "--copyright"),
        ("bundleId", "--bundle-id"),
        ("codesignIdentity", "--codesign-identity"),
    ):
        text = _first_text(options.get(option_key))
        if text:
            generated.extend([flag, text])

    _append_repeated_flag(generated, "--add-data", options.get("addData"), "source.options.addData")
    _append_repeated_flag(generated, "--add-binary", options.get("addBinary"), "source.options.addBinary")
    _append_repeated_flag(generated, "--hidden-import", options.get("hiddenImport"), "source.options.hiddenImport")
    _append_repeated_flag(
        generated,
        "--pyinstaller-build-args",
        options.get("pyinstallerBuildArgs"),
        "source.options.pyinstallerBuildArgs",
    )

    entry = _first_text(options.get("script"), options.get("entry"))
    if entry:
        generated.append(entry)
    return _ensure_noninteractive_args(generated)


def _ensure_noninteractive_args(args: list[str]) -> list[str]:
    if any(arg in {"-y", "--yes"} for arg in args):
        return args
    return ["--yes", *args]


def _build_prepared_result(command: str, input_data: dict[str, Any]) -> dict[str, Any]:
    project_dir = Path(_first_text(input_data.get("projectDir")) or Path.cwd()).resolve()
    options = _source_options(input_data)
    context = _context(input_data)
    flet_args = _resolve_flet_args(options)
    if not flet_args:
        raise ValueError("flet requires source.options.fletArgs or an entry script")

    parsed = project._parse_flet_metadata(flet_args)
    version = _first_text(input_data.get("version"), options.get("version"), parsed.product_version, project._read_pyproject_version(project_dir))
    if not version:
        raise ValueError("flet requires source.options.version, --product-version, or project.version in pyproject.toml")

    app_id = _first_text(
        context.get("appId"),
        context.get("publish", {}).get("appId"),
        options.get("appId"),
        options.get("appid"),
        parsed.bundle_id,
    )
    rc = _run_flet_pack_command(flet_args, project_dir)
    if rc != 0:
        raise RuntimeError(f"flet pack exited with code {rc}")

    config_data = project._build_distromate_project_config(
        project_dir,
        flet_args,
        app_id=app_id,
        base_context=context,
    )

    raw_source = config_data.get("source")
    source: dict[str, Any] = {}
    if isinstance(raw_source, dict):
        source = raw_source
    raw_package = config_data.get("package")
    package: dict[str, Any] = {}
    if isinstance(raw_package, dict):
        package = raw_package
    raw_publish = config_data.get("publish")
    publish: dict[str, Any] = {}
    if isinstance(raw_publish, dict):
        publish = raw_publish
    defaults: dict[str, Any] = {}
    resolved_version = _first_text(version)
    if resolved_version:
        defaults["version"] = resolved_version
    resolved_app_id = _first_text(config_data.get("appId"), publish.get("appId"), parsed.bundle_id)
    if resolved_app_id:
        defaults["appId"] = resolved_app_id
    product_name = _first_text(config_data.get("productName"))
    if product_name:
        defaults["productName"] = product_name
    icon = _first_text(config_data.get("icon"), package.get("icon"))
    if icon:
        defaults["icon"] = icon
    package_defaults: dict[str, Any] = {}
    package_name = _first_text(package.get("name"))
    if package_name:
        package_defaults["name"] = package_name
    package_description = _first_text(package.get("shortDescription"))
    if package_description:
        package_defaults["shortDescription"] = package_description
    package_icon = _first_text(package.get("icon"))
    if package_icon:
        package_defaults["icon"] = package_icon
    package_publisher = _first_text(package.get("publisher"))
    if package_publisher:
        package_defaults["publisher"] = package_publisher
    if package_defaults:
        defaults["package"] = package_defaults
    publish_defaults: dict[str, Any] = {}
    publish_app_id = _first_text(publish.get("appId"))
    if publish_app_id:
        publish_defaults["appId"] = publish_app_id
    if publish_defaults:
        defaults["publish"] = publish_defaults
    return {
        "defaults": defaults,
        "source": {
            "type": "native",
            "root": _first_text(source.get("root")),
            "executable": _first_text(source.get("executable")),
            "files": list(source.get("files") or []),
        },
        "cleanupPaths": [],
    }


def _run_flet_pack_command(args: list[str], project_dir: Path) -> int:
    cmd = [sys.executable, "-c", FLET_CLI_BOOTSTRAP, "pack", *args]
    print(f"[distromate-flet] $ flet pack {' '.join(args)}", file=sys.stderr)
    try:
        completed = subprocess.run(
            cmd,
            check=False,
            stdout=sys.stderr,
            stderr=sys.stderr,
            cwd=project_dir,
        )
    except FileNotFoundError:
        print(f"[distromate-flet] Command not found: {cmd[0]}", file=sys.stderr)
        return 127
    return completed.returncode


def _success_response(result: dict[str, Any] | None = None) -> dict[str, Any]:
    response: dict[str, Any] = {
        "protocol": PLUGIN_PROTOCOL,
        "version": PLUGIN_API_VERSION,
        "ok": True,
        "manifest": dict(PLUGIN_MANIFEST),
    }
    if result is not None:
        response["result"] = result
    return response


def _error_response(message: str) -> dict[str, Any]:
    return {
        "protocol": PLUGIN_PROTOCOL,
        "version": PLUGIN_API_VERSION,
        "ok": False,
        "error": {
            "message": message,
        },
    }


def main() -> int:
    try:
        request = _read_request()
        command, input_data = _validate_request(request)
        if command == "manifest":
            response = _success_response()
        else:
            response = _success_response(_build_prepared_result(command, input_data))
        sys.stdout.write(json.dumps(response) + "\n")
        return 0
    except Exception as exc:
        sys.stdout.write(json.dumps(_error_response(str(exc))) + "\n")
        return 1


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