use std::path::Path;

use anyhow::Result;

use super::super::{GenerateCI, Platform, ResolvedCIConfig, ResolvedTarget};
use super::resolve::resolve_config;
use super::yaml::Yaml;
use crate::BridgeModel;

/// Check if a given maturin-action field is uniform across all targets in the resolved list.
/// Returns Some(value) if all targets have the same value (or all None), None if they vary.
fn uniform_field<F: Fn(&ResolvedTarget) -> Option<&str>>(
    targets: &[ResolvedTarget],
    accessor: F,
) -> Option<Option<String>> {
    if targets.is_empty() {
        return Some(None);
    }
    let first = accessor(&targets[0]);
    for target in &targets[1..] {
        let value = accessor(target);
        if value != first {
            return None;
        }
    }
    Some(first.map(ToOwned::to_owned))
}

type FieldAccessor = fn(&ResolvedTarget) -> Option<&str>;

/// The list of maturin-action fields that can be configured per-target.
/// Each entry is (action_field_name with hyphens, matrix_key with underscores, accessor).
const MATURIN_ACTION_FIELDS: &[(&str, &str, FieldAccessor)] = &[
    ("manylinux", "manylinux", |target| {
        target.manylinux.as_deref()
    }),
    ("container", "container", |target| {
        target.container.as_deref()
    }),
    ("docker-options", "docker_options", |target| {
        target.docker_options.as_deref()
    }),
    ("rust-toolchain", "rust_toolchain", |target| {
        target.rust_toolchain.as_deref()
    }),
    ("rustup-components", "rustup_components", |target| {
        target.rustup_components.as_deref()
    }),
    ("before-script-linux", "before_script_linux", |target| {
        target.before_script_linux.as_deref()
    }),
];

// Bare GitHub Actions expression fragments (without `${{ }}`), used to build
// larger expressions without double-nesting.
const MATRIX_TARGET: &str = "matrix.platform.target";
const GITHUB_TOKEN: &str = "github.token";
const GITHUB_WORKSPACE: &str = "github.workspace";

pub(crate) fn generate_github_from_cli(
    cli: &GenerateCI,
    project_name: &str,
    bridge_model: &BridgeModel,
    sdist: bool,
) -> Result<String> {
    let resolved = resolve_config(cli, None, bridge_model)?;
    generate_github(cli, &resolved, project_name, bridge_model, sdist, None)
}

struct RenderContext<'a> {
    cli: &'a GenerateCI,
    resolved: &'a ResolvedCIConfig,
    project_name: &'a str,
    is_abi3: bool,
    is_bin: bool,
    setup_python: bool,
    /// Minimum Python 3 minor version from `requires-python` (e.g. 13 for `>=3.13`).
    min_python_minor: Option<u8>,
}

/// Generate GitHub Actions CI configuration.
pub(crate) fn generate_github(
    cli: &GenerateCI,
    resolved: &ResolvedCIConfig,
    project_name: &str,
    bridge_model: &BridgeModel,
    sdist: bool,
    min_python_minor: Option<u8>,
) -> Result<String> {
    let is_abi3 = bridge_model.has_stable_abi();
    let is_bin = bridge_model.is_bin();
    let setup_python = resolved.pytest
        || matches!(
            bridge_model,
            BridgeModel::Bin(Some(_))
                | BridgeModel::PyO3 { .. }
                | BridgeModel::Cffi
                | BridgeModel::UniFfi
        );

    let context = RenderContext {
        cli,
        resolved,
        project_name,
        is_abi3,
        is_bin,
        setup_python,
        min_python_minor,
    };

    let mut conf = workflow_header();
    let mut needs = Vec::new();

    for (&platform, targets) in &resolved.platform_targets {
        needs.push(platform.to_string());
        emit_platform_job(&mut conf, platform, targets, &context);
        conf.push('\n');
    }

    if sdist {
        needs.push("sdist".to_string());
        emit_sdist_job(&mut conf, cli);
        conf.push('\n');
    }

    emit_release_job(&mut conf, resolved, &needs);
    Ok(conf)
}

fn workflow_header() -> String {
    let mut gen_cmd = std::env::args()
        .enumerate()
        .map(|(index, arg)| {
            if index == 0 {
                env!("CARGO_PKG_NAME").to_string()
            } else {
                arg
            }
        })
        .collect::<Vec<String>>()
        .join(" ");
    if gen_cmd.starts_with("maturin new") || gen_cmd.starts_with("maturin init") {
        gen_cmd = format!("{} generate-ci github", env!("CARGO_PKG_NAME"));
    }

    format!(
        "# This file is autogenerated by maturin v{version}
# To update, run
#
#    {gen_cmd}
#
name: CI

on:
  push:
    branches:
      - main
      - master
    tags:
      - '*'
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

jobs:\n",
        version = env!("CARGO_PKG_VERSION"),
    )
}

fn emit_platform_job(
    conf: &mut String,
    platform: Platform,
    targets: &[ResolvedTarget],
    context: &RenderContext<'_>,
) {
    let platform_name = platform.to_string();
    let mut y = Yaml::new(conf, 1);
    y.line(format!("{platform_name}:"))
        .indent()
        .line(format!("runs-on: {}", gha_expr("matrix.platform.runner")));

    emit_target_matrix(&mut y, targets);

    y.line("steps:")
        .indent()
        .line("- uses: actions/checkout@v6");
    emit_platform_setup(
        &mut y,
        platform,
        context.setup_python,
        context.min_python_minor,
    );

    let maturin_args = build_maturin_args(
        context.cli,
        context.resolved,
        platform,
        context.is_abi3,
        context.is_bin,
        context.setup_python,
    );
    let extra_args_suffix = build_extra_args_suffix(targets);

    emit_build_step(
        &mut y,
        "Build wheels",
        &maturin_args,
        &extra_args_suffix,
        "",
        targets,
    );

    if context.is_abi3 {
        emit_free_threaded_setup(&mut y, platform, context.min_python_minor);
        emit_build_step(
            &mut y,
            "Build free-threaded wheels",
            &maturin_args,
            &extra_args_suffix,
            " -i python3.14t",
            targets,
        );
    }

    emit_upload_wheels_step(&mut y, platform);

    if context.resolved.pytest {
        let chdir = pytest_chdir_prefix(context.cli);
        emit_pytest_steps(
            &mut y,
            platform,
            context.project_name,
            &chdir,
            context.min_python_minor,
        );
    }
}

fn emit_target_matrix(y: &mut Yaml, targets: &[ResolvedTarget]) {
    if targets.is_empty() {
        return;
    }

    y.line("strategy:")
        .indent()
        .line("matrix:")
        .indent()
        .line("platform:")
        .indent();
    for target in targets {
        y.line(format!("- runner: {}", target.runner)).indent();
        y.line(format!("target: {}", target.target));
        if let Some(python_arch) = &target.python_arch {
            y.line(format!("python_arch: {python_arch}"));
        }
        emit_varying_matrix_fields(y, targets, target);
        y.dedent();
    }
    y.dedent_by(3);
}

fn emit_platform_setup(
    y: &mut Yaml,
    platform: Platform,
    setup_python: bool,
    min_python_minor: Option<u8>,
) {
    match platform {
        Platform::Emscripten => emit_emscripten_setup(y),
        Platform::Android => {}
        _ if setup_python => emit_python_setup(y, platform, min_python_minor),
        _ => {}
    }
}

fn emit_emscripten_setup(y: &mut Yaml) {
    y.line("- run: pip install pyodide-build");
    y.line("- name: Get Emscripten and Python version info")
        .indent()
        .line("shell: bash")
        .line("run: |")
        .indent();
    y.line("echo EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version) >> $GITHUB_ENV");
    y.line("echo PYTHON_VERSION=$(pyodide config get python_version | cut -d '.' -f 1-2) >> $GITHUB_ENV");
    // Pre-PEP 783 ABI / PEP 783 platform-tag input. Pyodide >= 0.28 exposes
    // `pyodide_abi_version` (e.g. `2025_0` for 0.28/0.29 on Python 3.13,
    // `2026_0` for 0.30 / 314.0.0a1 on Python 3.14). `pyodide config get`
    // exits 1 and prints an error to stdout for unknown keys, so we use an
    // `if` to only export `PYODIDE_ABI_VERSION` when the key is recognised.
    // Older Pyodide releases simply leave it unset and maturin falls back to
    // the legacy `emscripten_*` tag. PEP 783 (pyemscripten_*) is then
    // selected by the cascade in `src/target/platform_tag.rs` based on
    // `PYTHON_VERSION` (>= 3.14).
    y.line(
        "if v=$(pyodide config get pyodide_abi_version 2>/dev/null); then echo PYODIDE_ABI_VERSION=$v >> $GITHUB_ENV; fi",
    );
    y.line("pip uninstall -y pyodide-build");
    y.dedent_by(2)
        .line("- uses: mymindstorm/setup-emsdk@v14")
        .indent()
        .line("with:")
        .indent()
        .line(format!("version: {}", gha_expr("env.EMSCRIPTEN_VERSION")))
        .line("actions-cache-folder: emsdk-cache")
        .dedent_by(2)
        .line("- uses: actions/setup-python@v6")
        .indent()
        .line("with:")
        .indent()
        .line(format!(
            "python-version: {}",
            gha_expr("env.PYTHON_VERSION")
        ))
        .dedent_by(2)
        .line("- run: pip install pyodide-build");
}

fn emit_python_setup(y: &mut Yaml, platform: Platform, min_python_minor: Option<u8>) {
    let python_version = match (platform, min_python_minor) {
        (Platform::Windows, Some(minor)) if minor >= 13 => format!("3.{minor}"),
        (Platform::Windows, _) => "3.13".to_string(),
        (_, Some(minor)) => format!("3.{minor}"),
        _ => "3.x".to_string(),
    };

    y.line("- uses: actions/setup-python@v6")
        .indent()
        .line("with:")
        .indent()
        .line(format!("python-version: \"{python_version}\""));
    if matches!(platform, Platform::Windows) {
        y.line(format!(
            "architecture: {}",
            gha_expr("matrix.platform.python_arch")
        ));
    }
    y.dedent_by(2);
}

fn emit_free_threaded_setup(y: &mut Yaml, platform: Platform, min_python_minor: Option<u8>) {
    if !matches!(platform, Platform::Windows | Platform::Macos) {
        return;
    }

    let python_version = if matches!(min_python_minor, Some(minor) if minor > 14) {
        format!("3.{}t", min_python_minor.unwrap())
    } else {
        "3.14t".to_string()
    };

    y.line("- uses: actions/setup-python@v6")
        .indent()
        .line("with:")
        .indent()
        .line(format!("python-version: \"{python_version}\""));
    if matches!(platform, Platform::Windows) {
        y.line(format!(
            "architecture: {}",
            gha_expr("matrix.platform.python_arch")
        ));
    }
    y.dedent_by(2);
}

fn build_maturin_args(
    cli: &GenerateCI,
    resolved: &ResolvedCIConfig,
    platform: Platform,
    is_abi3: bool,
    is_bin: bool,
    setup_python: bool,
) -> String {
    let mut maturin_args = if is_abi3 || (is_bin && !setup_python) {
        Vec::new()
    } else if matches!(platform, Platform::Emscripten) {
        vec!["-i".to_string(), "${{ env.PYTHON_VERSION }}".to_string()]
    } else if matches!(platform, Platform::Android) {
        Vec::new()
    } else {
        vec!["--find-interpreter".to_string()]
    };

    if let Some(manifest_arg) = manifest_arg(cli) {
        maturin_args.push("--manifest-path".to_string());
        maturin_args.push(manifest_arg);
    }
    if resolved.zig && matches!(platform, Platform::ManyLinux) {
        maturin_args.push("--zig".to_string());
    }

    if maturin_args.is_empty() {
        String::new()
    } else {
        format!(" {}", maturin_args.join(" "))
    }
}

fn build_extra_args_suffix(targets: &[ResolvedTarget]) -> String {
    if let Some(uniform_value) = uniform_field(targets, |target| target.extra_args.as_deref()) {
        uniform_value
            .map(|value| format!(" {value}"))
            .unwrap_or_default()
    } else {
        " ${{ matrix.platform.extra_args }}".to_string()
    }
}

fn manifest_arg(cli: &GenerateCI) -> Option<String> {
    cli.manifest_path.as_ref().and_then(|manifest_path| {
        (manifest_path != Path::new("Cargo.toml")).then(|| manifest_path.display().to_string())
    })
}

fn pytest_chdir_prefix(cli: &GenerateCI) -> String {
    cli.manifest_path
        .as_ref()
        .and_then(|manifest_path| {
            (manifest_path != Path::new("Cargo.toml"))
                .then(|| manifest_path.parent().unwrap().display().to_string())
        })
        .map(|parent| format!("cd {parent} && "))
        .unwrap_or_default()
}

fn emit_upload_wheels_step(y: &mut Yaml, platform: Platform) {
    let artifact_name = match platform {
        Platform::Emscripten => "wasm-wheels".to_string(),
        _ => format!("wheels-{platform}-{}", gha_expr("matrix.platform.target")),
    };

    y.line("- name: Upload wheels")
        .indent()
        .line("uses: actions/upload-artifact@v6")
        .line("with:")
        .indent()
        .line(format!("name: {artifact_name}"))
        .line("path: dist")
        .dedent_by(2);
}

fn emit_sdist_job(conf: &mut String, cli: &GenerateCI) {
    let manifest_args = manifest_arg(cli)
        .map(|path| format!(" --manifest-path {path}"))
        .unwrap_or_default();

    let mut y = Yaml::new(conf, 1);
    y.line("sdist:")
        .indent()
        .line("runs-on: ubuntu-latest")
        .line("steps:")
        .indent()
        .line("- uses: actions/checkout@v6")
        .line("- name: Build sdist")
        .indent()
        .line("uses: PyO3/maturin-action@v1")
        .line("with:")
        .indent()
        .line("command: sdist")
        .line(format!("args: --out dist{manifest_args}"))
        .dedent_by(2)
        .line("- name: Upload sdist")
        .indent()
        .line("uses: actions/upload-artifact@v6")
        .line("with:")
        .indent()
        .line("name: wheels-sdist")
        .line("path: dist");
}

fn emit_release_job(conf: &mut String, resolved: &ResolvedCIConfig, needs: &[String]) {
    let mut y = Yaml::new(conf, 1);
    y.line("release:");
    y.indent();
    y.line("name: Release");
    y.line("runs-on: ubuntu-latest");
    y.line(format!(
        "if: {}",
        gha_expr(
            "startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'"
        )
    ));
    y.line(format!("needs: [{}]", needs.join(", ")));

    if let Some(environment) = &resolved.publishing_environment {
        y.line(format!("environment: {environment}"));
    }

    emit_release_permissions(&mut y, resolved);
    emit_release_steps(&mut y, resolved);
}

fn emit_release_permissions(y: &mut Yaml, resolved: &ResolvedCIConfig) {
    y.line("permissions:");
    y.indent();
    y.line("# Use to sign the release artifacts");
    y.line("id-token: write");
    y.line("# Used to upload release artifacts");
    y.line("contents: write");
    if !resolved.skip_attestation {
        y.line("# Used to generate artifact attestation");
        y.line("attestations: write");
    }
    y.dedent();
}

fn emit_release_steps(y: &mut Yaml, resolved: &ResolvedCIConfig) {
    y.line("steps:");
    y.indent();
    y.line("- uses: actions/download-artifact@v7");

    if !resolved.skip_attestation {
        emit_release_attestation_step(y);
    }
    emit_release_publish_steps(y, resolved);
    if resolved
        .platform_targets
        .contains_key(&Platform::Emscripten)
    {
        emit_release_github_upload_step(y);
    }
}

fn emit_release_attestation_step(y: &mut Yaml) {
    y.line("- name: Generate artifact attestation");
    y.indent();
    y.line("uses: actions/attest@v4");
    y.line("with:");
    y.indent();
    y.line("subject-path: 'wheels-*/*'");
    y.dedent_by(2);
}

fn emit_release_publish_steps(y: &mut Yaml, resolved: &ResolvedCIConfig) {
    y.line("- name: Install uv");
    y.indent();
    y.line(format!(
        "if: {}",
        gha_expr("startsWith(github.ref, 'refs/tags/')")
    ));
    y.line("uses: astral-sh/setup-uv@v7");
    y.dedent();

    y.line("- name: Publish to PyPI");
    y.indent();
    y.line(format!(
        "if: {}",
        gha_expr("startsWith(github.ref, 'refs/tags/')")
    ));
    if resolved.trusted_publishing {
        y.line("run: uv publish --trusted-publishing always 'wheels-*/*'");
        y.dedent();
    } else {
        y.line("run: uv publish 'wheels-*/*'");
        y.line("env:");
        y.indent();
        y.line(format!(
            "UV_PUBLISH_TOKEN: {}",
            gha_expr("secrets.PYPI_API_TOKEN")
        ));
        y.dedent_by(2);
    }
}

fn emit_release_github_upload_step(y: &mut Yaml) {
    y.line("- name: Upload to GitHub Release");
    y.indent();
    y.line("uses: softprops/action-gh-release@v1");
    y.line("with:");
    y.indent();
    y.line("files: |");
    y.indent();
    y.line("wasm-wheels/*.whl");
    y.dedent();
    y.line(format!(
        "prerelease: {}",
        gha_expr("contains(github.ref, 'alpha') || contains(github.ref, 'beta')")
    ));
}

/// Wrap a bare expression in `${{ ... }}` for use as a YAML value.
fn gha_expr(expr: &str) -> String {
    format!("${{{{ {expr} }}}}")
}

/// Emit pytest steps for a given platform.
fn emit_pytest_steps(
    y: &mut Yaml,
    platform: Platform,
    project_name: &str,
    chdir: &str,
    min_python_minor: Option<u8>,
) {
    match platform {
        Platform::All | Platform::Android => {}
        Platform::ManyLinux => emit_manylinux_pytest(y, project_name, chdir, min_python_minor),
        Platform::Musllinux => emit_musllinux_pytest(y, project_name, chdir),
        Platform::Windows => {
            emit_uv_pytest(
                y,
                project_name,
                chdir,
                ".venv/Scripts/activate",
                None,
                Some("bash"),
            );
        }
        Platform::Macos => {
            emit_uv_pytest(y, project_name, chdir, ".venv/bin/activate", None, None);
        }
        Platform::Emscripten => emit_emscripten_pytest(y, project_name, chdir),
    }
}

/// Ubuntu 24.04 ships Python 3.12; versions above this need the deadsnakes PPA.
const UBUNTU_2404_PYTHON_MINOR: u8 = 12;

/// Emit pytest steps for manylinux: host test (x86_64) + QEMU cross-arch test.
fn emit_manylinux_pytest(
    y: &mut Yaml,
    project_name: &str,
    chdir: &str,
    min_python_minor: Option<u8>,
) {
    let if_x86_64 = gha_expr(&format!("startsWith({MATRIX_TARGET}, 'x86_64')"));

    y.line("- uses: astral-sh/setup-uv@v7")
        .indent()
        .line(format!("if: {if_x86_64}"))
        .dedent();

    emit_uv_pytest(
        y,
        project_name,
        chdir,
        ".venv/bin/activate",
        Some(&if_x86_64),
        Some("bash"),
    );

    let needs_deadsnakes =
        matches!(min_python_minor, Some(minor) if minor > UBUNTU_2404_PYTHON_MINOR);

    y.line("- name: pytest")
        .indent()
        .line(format!(
            "if: {}",
            gha_expr(&format!(
                "!startsWith({MATRIX_TARGET}, 'x86') && {MATRIX_TARGET} != 'ppc64'"
            ))
        ))
        .line("uses: uraimo/run-on-arch-action@v2")
        .line("with:")
        .indent();
    y.line(format!("arch: {}", gha_expr(MATRIX_TARGET)));
    y.line("distro: ubuntu24.04");
    y.line(format!("githubToken: {}", gha_expr(GITHUB_TOKEN)));
    y.line("install: |");
    y.indent();
    y.line("apt-get update");
    if needs_deadsnakes {
        let minor = min_python_minor.unwrap();
        y.line("apt-get install -y --no-install-recommends software-properties-common");
        y.line("add-apt-repository -y ppa:deadsnakes/ppa");
        y.line("apt-get update");
        y.line(format!(
            "apt-get install -y --no-install-recommends python3.{minor} python3.{minor}-venv"
        ));
    } else {
        y.line("apt-get install -y --no-install-recommends python3 python3-venv");
    }
    y.dedent();
    y.line("run: |");
    y.indent();
    y.line("set -e");
    if needs_deadsnakes {
        let minor = min_python_minor.unwrap();
        y.line(format!("python3.{minor} -m venv .venv"));
    } else {
        y.line("python3 -m venv .venv");
    }
    y.line("source .venv/bin/activate");
    y.line("pip install pytest");
    y.line(format!(
        "pip install {project_name} --no-index --no-deps --find-links dist --force-reinstall"
    ));
    y.line(format!("pip install {project_name}"));
    y.line(format!("{chdir}pytest"));
    y.dedent_by(3);
}

/// Emit pytest steps for musllinux: docker (x86_64) + QEMU cross-arch.
fn emit_musllinux_pytest(y: &mut Yaml, project_name: &str, chdir: &str) {
    let if_x86_64 = gha_expr(&format!("startsWith({MATRIX_TARGET}, 'x86_64')"));
    let if_not_x86 = gha_expr(&format!("!startsWith({MATRIX_TARGET}, 'x86')"));

    y.line("- name: pytest")
        .indent()
        .line(format!("if: {if_x86_64}"))
        .line("run: |")
        .indent();
    y.line("set -e");
    y.line(format!(
        "docker run --rm -v {}:/io -w /io alpine:latest sh -c '",
        gha_expr(GITHUB_WORKSPACE)
    ));
    y.line("  apk add py3-pip py3-virtualenv");
    y.line("  python3 -m virtualenv .venv");
    y.line("  source .venv/bin/activate");
    y.line(format!(
        "  pip install {project_name} --no-index --no-deps --find-links dist --force-reinstall"
    ));
    y.line(format!("  pip install {project_name} pytest"));
    y.line(format!("  {chdir}pytest"));
    y.line("'")
        .dedent_by(2)
        .line("- name: pytest")
        .indent()
        .line(format!("if: {if_not_x86}"))
        .line("uses: uraimo/run-on-arch-action@v2")
        .line("with:")
        .indent();
    y.line(format!("arch: {}", gha_expr(MATRIX_TARGET)));
    y.line("distro: alpine_latest");
    y.line(format!("githubToken: {}", gha_expr(GITHUB_TOKEN)));
    y.line("install: |");
    y.indent();
    y.line("apk add py3-virtualenv");
    y.dedent();
    y.line("run: |");
    y.indent();
    y.line("set -e");
    y.line("python3 -m virtualenv .venv");
    y.line("source .venv/bin/activate");
    y.line(format!(
        "pip install {project_name} --no-index --no-deps --find-links dist --force-reinstall"
    ));
    y.line(format!("pip install {project_name} pytest"));
    y.line(format!("{chdir}pytest"));
    y.dedent_by(3);
}

/// Emit uv-based pytest steps (shared by manylinux host, windows, macos).
fn emit_uv_pytest(
    y: &mut Yaml,
    project_name: &str,
    chdir: &str,
    activate_path: &str,
    if_expr: Option<&str>,
    shell: Option<&str>,
) {
    if if_expr.is_none() {
        y.line("- uses: astral-sh/setup-uv@v7");
    }
    y.line("- name: pytest").indent();
    if let Some(expr) = if_expr {
        y.line(format!("if: {expr}"));
    }
    if let Some(shell) = shell {
        y.line(format!("shell: {shell}"));
    }
    y.line("run: |").indent();
    y.line("set -e");
    y.line("uv venv .venv");
    y.line(format!("source {activate_path}"));
    y.line(format!(
        "uv pip install {project_name} --no-index --no-deps --find-links dist --reinstall"
    ));
    y.line(format!("uv pip install {project_name} pytest"));
    y.line(format!("{chdir}pytest"));
    y.dedent_by(2);
}

/// Emit pytest steps for emscripten/pyodide.
fn emit_emscripten_pytest(y: &mut Yaml, project_name: &str, chdir: &str) {
    y.line("- uses: actions/setup-node@v3")
        .indent()
        .line("with:")
        .indent()
        .line("node-version: '22'")
        .dedent_by(2)
        .line("- name: pytest")
        .indent()
        .line("run: |")
        .indent();
    y.line("set -e");
    y.line("pyodide venv .venv");
    y.line("source .venv/bin/activate");
    y.line(format!(
        "pip install {project_name} --no-index --no-deps --find-links dist --force-reinstall"
    ));
    y.line(format!("pip install {project_name} pytest"));
    y.line(format!("{chdir}python -m pytest"));
    y.dedent_by(2);
}

/// Emit a maturin-action build step with shared fields.
fn emit_build_step(
    y: &mut Yaml,
    name: &str,
    maturin_args: &str,
    extra_args_suffix: &str,
    extra_suffix: &str,
    targets: &[ResolvedTarget],
) {
    y.line(format!("- name: {name}"))
        .indent()
        .line("uses: PyO3/maturin-action@v1")
        .line("with:")
        .indent();
    y.line(format!("target: {}", gha_expr(MATRIX_TARGET)));
    y.line(format!(
        "args: --release --out dist{maturin_args}{extra_args_suffix}{extra_suffix}"
    ));
    y.line(format!(
        "sccache: {}",
        gha_expr("!startsWith(github.ref, 'refs/tags/')")
    ));
    for &(field_name, matrix_key, accessor) in MATURIN_ACTION_FIELDS {
        emit_maturin_action_field(y, field_name, matrix_key, targets, accessor);
    }
    y.dedent_by(2);
}

/// Emit a maturin-action field either at step level (uniform) or as matrix reference.
fn emit_maturin_action_field(
    y: &mut Yaml,
    field_name: &str,
    matrix_key: &str,
    targets: &[ResolvedTarget],
    accessor: FieldAccessor,
) {
    if let Some(uniform_value) = uniform_field(targets, accessor) {
        if let Some(value) = uniform_value {
            y.line(format!("{field_name}: {value}"));
        }
    } else {
        y.line(format!(
            "{field_name}: ${{{{ matrix.platform.{matrix_key} }}}}"
        ));
    }
}

/// Emit varying maturin-action fields into matrix entries for a specific target.
fn emit_varying_matrix_fields(
    y: &mut Yaml,
    all_targets: &[ResolvedTarget],
    target: &ResolvedTarget,
) {
    for &(_field_name, matrix_key, accessor) in MATURIN_ACTION_FIELDS {
        if uniform_field(all_targets, accessor).is_none()
            && let Some(value) = accessor(target)
        {
            y.line(format!("{matrix_key}: {value}"));
        }
    }
    if uniform_field(all_targets, |target| target.extra_args.as_deref()).is_none()
        && let Some(value) = &target.extra_args
    {
        y.line(format!("extra_args: {value}"));
    }
}
