use std::path::Path;

use miette::{Context, IntoDiagnostic};

use crate::codegen;
use crate::github::parse_action_ref;
use crate::lockfile::Lockfile;

const API_DTS: &str = include_str!("../runtime/api.d.ts");

const CHECK_WORKFLOW_TS: &str = r#"
// Feel free to edit this file, subsequent runs of `ghat init` won't regenerate it.

// For convenience, here are some configuration options:
/** The runner used for the check job. */
const RUNNER = "ubuntu-latest";
/** The download URL, in case you want to use a mirror instead. */
const DOWNLOAD_URL = "https://github.com/jprochazk/ghat/releases/latest/download/ghat-installer.sh"

workflow("ghat check", {
  on: triggers({
    push: [],
  }),

  jobs(ctx) {
    ctx.job("Check", {
      runs_on: RUNNER,

      steps() {
        uses("actions/checkout", {
            // We only need to look at the latest commit
            with: { fetch_depth: "1" }
        })
        run(`curl --proto '=https' --tlsv1.2 -LsSf ${DOWNLOAD_URL} | sh`)
        run("ghat generate")
        run("git diff --exit-code")
      },
    })
  },
})
"#;

/// Common actions added to every new project.
const DEFAULT_ACTIONS: &[&str] = &[
    "actions/checkout",
    "actions/cache",
    "actions/upload-artifact",
    "actions/download-artifact",
];

const TSCONFIG: &str = r#"{
  "compilerOptions": {
    "strict": true,
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["workflows/**/*.ts", "types/**/*.d.ts", "actions/**/*.d.ts"]
}
"#;

pub fn run(github_token: Option<String>) -> miette::Result<()> {
    let bare = std::env::var("__GHAT_INIT_BARE").is_ok();
    let base = Path::new(super::common::BASE_DIR);

    if base.exists() {
        log::info!("{} already exists", base.display());
    }

    let dirs = [
        base.join("types"),
        base.join("workflows"),
        base.join("actions"),
    ];
    for dir in &dirs {
        std::fs::create_dir_all(dir)
            .into_diagnostic()
            .wrap_err_with(|| format!("failed to create directory: {}", dir.display()))?;
    }

    let api_dts = format!("// Do not edit: this file is generated by `ghat generate`.\n{API_DTS}");
    write_if_changed(&base.join("types/api.d.ts"), &api_dts)?;
    write_if_changed(&base.join("tsconfig.json"), TSCONFIG)?;

    write_if_changed(
        &base.join(".gitattributes"),
        "actions/** linguist-generated\ntypes/** linguist-generated\n",
    )?;

    let lockfile_path = base.join("ghat.lock");
    let fresh_lockfile = !lockfile_path.exists();
    if fresh_lockfile {
        std::fs::write(&lockfile_path, "")
            .into_diagnostic()
            .wrap_err("failed to create lockfile")?;
        log::info!("created {}", lockfile_path.display());
    }

    if !bare {
        // Migration: remove the old hardcoded ghat_check.yaml (replaced by
        // ghat_check.ts). This can be removed in a later version.
        let old_check_workflow = Path::new(".github/workflows/ghat_check.yaml");
        if old_check_workflow.exists() {
            std::fs::remove_file(old_check_workflow)
                .into_diagnostic()
                .wrap_err("failed to remove old ghat_check.yaml")?;
            super::style::status("Removed", "old .github/workflows/ghat_check.yaml");
        }

        // Create the ghat_check workflow definition if it doesn't exist yet.
        let check_workflow_path = base.join("workflows/ghat_check.ts");
        if !check_workflow_path.exists() {
            write_if_changed(&check_workflow_path, CHECK_WORKFLOW_TS)?;
        }

        // Add any missing default actions to the lockfile.
        add_default_actions(&lockfile_path, github_token)?;
    }

    super::style::status("Initialized", format!("ghat project in {}", base.display()));
    Ok(())
}

/// Resolve and add the default set of common actions to the lockfile.
fn add_default_actions(lockfile_path: &Path, github_token: Option<String>) -> miette::Result<()> {
    let mut lockfile = Lockfile::load(lockfile_path)?;
    let client = super::common::github_client(github_token);

    let refs: Vec<_> = DEFAULT_ACTIONS
        .iter()
        .map(|s| parse_action_ref(s))
        .collect::<miette::Result<Vec<_>>>()?;

    let results = super::add::add_actions(&client, &mut lockfile, &refs)?;
    lockfile.save(lockfile_path)?;

    let base = super::common::base_dir();
    let mut cache = codegen::ManifestCache::load(&base.join("actions/cache.json"))?;

    for r in &results {
        match r {
            super::add::AddResult::Added { name, resolved } => {
                let manifest = codegen::get_or_fetch_manifest(
                    &mut cache,
                    &client,
                    name,
                    &resolved.sha,
                    &resolved.version,
                )?;
                codegen::write_action_types(base, name, manifest)?;
                super::style::status(
                    "Added",
                    format!("{name} {} ({})", resolved.version, &resolved.sha[..12]),
                );
            }
            super::add::AddResult::Skipped { .. } => {}
        }
    }

    super::common::finalize_codegen(&lockfile, &mut cache)?;

    Ok(())
}

/// Write a file only if the contents have changed (or it doesn't exist yet).
fn write_if_changed(path: &Path, contents: &str) -> miette::Result<()> {
    if path.exists() {
        let existing = std::fs::read_to_string(path)
            .into_diagnostic()
            .wrap_err_with(|| format!("failed to read {}", path.display()))?;
        if existing == contents {
            log::debug!("{} is up to date", path.display());
            return Ok(());
        }
        log::info!("updating {}", path.display());
    } else {
        log::info!("creating {}", path.display());
    }

    std::fs::write(path, contents)
        .into_diagnostic()
        .wrap_err_with(|| format!("failed to write {}", path.display()))?;
    Ok(())
}
