// file: poc-qt/poc003/build.rs

use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

use cxx_qt_build::{CxxQtBuilder, QmlModule};
use serde::Deserialize;
use serde_json::Value;

const DEFAULT_CONFIG_STR: &str = include_str!("build_config.default.json");
const CONFIG_SCHEMA_STR: &str = include_str!("../build_config.schema.json");

#[derive(Debug, Deserialize)]
struct BuildConfig {
    active_profile: String,
    profiles: BTreeMap<String, BuildProfile>,
}

#[derive(Debug, Deserialize)]
struct BuildProfile {
    qt: QtConfig,
    link: LinkConfig,
}

#[derive(Debug, Deserialize)]
struct QtConfig {
    qmake: String,
    lib_dir: String,
    qml_dir: String,
    plugins_dir: String,
    version_major: u8,
}

#[derive(Debug, Deserialize)]
struct LinkConfig {
    rpaths: Vec<String>,
}

fn ensure_user_config_exists(config_path: &Path) {
    if config_path.exists() {
        return;
    }

    match fs::write(config_path, DEFAULT_CONFIG_STR) {
        Ok(()) => {
            println!(
                "cargo:warning=build_config.json was missing and has been created from build_config.default.json"
            );
        }
        Err(err) => {
            panic!(
                "failed to create {} from embedded defaults: {}",
                config_path.display(),
                err
            );
        }
    }
}

fn validate_embedded_schema() {
    let schema_json = parse_json_str(CONFIG_SCHEMA_STR, "embedded build_config.schema.json");

    match jsonschema::meta::validate(&schema_json) {
        Ok(()) => {}
        Err(err) => {
            panic!("embedded build_config.schema.json is not a valid JSON Schema: {err}");
        }
    }
}

fn validate_default_config() {
    validate_config_str_against_schema(DEFAULT_CONFIG_STR, "embedded build_config.default.json");
}

fn validate_user_config(config_path: &Path) {
    let raw = match fs::read_to_string(config_path) {
        Ok(v) => v,
        Err(err) => {
            panic!("failed to read {}: {}", config_path.display(), err);
        }
    };

    validate_config_str_against_schema(&raw, &format!("{}", config_path.display()));
}

fn validate_config_str_against_schema(config_raw: &str, config_name: &str) {
    let schema_json = parse_json_str(CONFIG_SCHEMA_STR, "embedded build_config.schema.json");
    let config_json = parse_json_str(config_raw, config_name);

    let validator = match jsonschema::validator_for(&schema_json) {
        Ok(v) => v,
        Err(err) => {
            panic!(
                "failed to build JSON Schema validator for {}: {}",
                config_name, err
            );
        }
    };

    if validator.is_valid(&config_json) {
        return;
    }

    let mut errors = Vec::new();
    for err in validator.iter_errors(&config_json) {
        errors.push(format!(
            "- {} at instance path {}",
            err,
            err.instance_path()
        ));
    }

    let joined = errors.join("\n");
    panic!("{} failed schema validation:\n{}", config_name, joined);
}

fn load_build_config(config_path: &Path) -> BuildConfig {
    let raw = match fs::read_to_string(config_path) {
        Ok(v) => v,
        Err(err) => {
            panic!("failed to read {}: {}", config_path.display(), err);
        }
    };

    match serde_json::from_str::<BuildConfig>(&raw) {
        Ok(v) => v,
        Err(err) => {
            panic!(
                "failed to deserialize {} into BuildConfig: {}",
                config_path.display(),
                err
            );
        }
    }
}

fn resolve_profile_name(build_config: &BuildConfig) -> String {
    let env_profile = std::env::var("BUILD_CONFIG_PROFILE");
    match env_profile {
        Ok(v) => {
            if v.trim().is_empty() {
                build_config.active_profile.clone()
            } else {
                v
            }
        }
        Err(_) => build_config.active_profile.clone(),
    }
}

fn resolve_profile<'a>(build_config: &'a BuildConfig, profile_name: &str) -> &'a BuildProfile {
    match build_config.profiles.get(profile_name) {
        Some(v) => v,
        None => {
            let mut available = Vec::new();
            for key in build_config.profiles.keys() {
                available.push(key.clone());
            }
            panic!(
                "active profile '{}' not found in build_config.json. available profiles: {}",
                profile_name,
                available.join(", ")
            );
        }
    }
}

fn validate_profile_paths(profile: &BuildProfile, profile_name: &str) {
    ensure_existing_file(&profile.qt.qmake, profile_name, "qt.qmake");
    ensure_existing_dir(&profile.qt.lib_dir, profile_name, "qt.lib_dir");
    ensure_existing_dir(&profile.qt.qml_dir, profile_name, "qt.qml_dir");
    ensure_existing_dir(&profile.qt.plugins_dir, profile_name, "qt.plugins_dir");

    let mut index = 0usize;
    while index < profile.link.rpaths.len() {
        ensure_existing_dir(
            &profile.link.rpaths[index],
            profile_name,
            &format!("link.rpaths[{index}]"),
        );
        index += 1;
    }
}

fn ensure_existing_file(path_str: &str, profile_name: &str, field_name: &str) {
    let path = Path::new(path_str);
    if !path.exists() {
        panic!(
            "profile '{}' has invalid {}: path does not exist: {}",
            profile_name,
            field_name,
            path.display()
        );
    }
    if !path.is_file() {
        panic!(
            "profile '{}' has invalid {}: path is not a file: {}",
            profile_name,
            field_name,
            path.display()
        );
    }
}

fn ensure_existing_dir(path_str: &str, profile_name: &str, field_name: &str) {
    let path = Path::new(path_str);
    if !path.exists() {
        panic!(
            "profile '{}' has invalid {}: path does not exist: {}",
            profile_name,
            field_name,
            path.display()
        );
    }
    if !path.is_dir() {
        panic!(
            "profile '{}' has invalid {}: path is not a directory: {}",
            profile_name,
            field_name,
            path.display()
        );
    }
}

fn apply_build_environment(profile: &BuildProfile) {
    unsafe {
        std::env::set_var("QMAKE", &profile.qt.qmake);
        std::env::set_var("QT_VERSION_MAJOR", profile.qt.version_major.to_string());
    }

    println!("cargo:rerun-if-env-changed=BUILD_CONFIG_PROFILE");
    println!("cargo:rerun-if-env-changed=QMAKE");
    println!("cargo:rerun-if-env-changed=QT_VERSION_MAJOR");
}

fn apply_linker_settings(profile: &BuildProfile) {
    let mut index = 0usize;
    while index < profile.link.rpaths.len() {
        println!(
            "cargo:rustc-link-arg=-Wl,-rpath,{}",
            profile.link.rpaths[index]
        );
        index += 1;
    }
}

fn emit_runtime_hints(profile: &BuildProfile, profile_name: &str) {
    println!(
        "cargo:warning=using build profile '{}' with qmake '{}'",
        profile_name, profile.qt.qmake
    );
    println!(
        "cargo:warning=qt lib_dir='{}', qml_dir='{}', plugins_dir='{}'",
        profile.qt.lib_dir, profile.qt.qml_dir, profile.qt.plugins_dir
    );
}

fn parse_json_str(raw: &str, label: &str) -> Value {
    match serde_json::from_str::<Value>(raw) {
        Ok(v) => v,
        Err(err) => {
            panic!("failed to parse {} as JSON: {}", label, err);
        }
    }
}

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=build_config.default.json");
    println!("cargo:rerun-if-changed=build_config.schema.json");
    println!("cargo:rerun-if-changed=qml/main.qml");

    let config_path = PathBuf::from("build_config.json");

    ensure_user_config_exists(&config_path);
    validate_embedded_schema();
    validate_default_config();
    validate_user_config(&config_path);

    let build_config = load_build_config(&config_path);
    let profile_name = resolve_profile_name(&build_config);
    let profile = resolve_profile(&build_config, &profile_name);

    validate_profile_paths(profile, &profile_name);
    apply_build_environment(profile);
    apply_linker_settings(profile);

    CxxQtBuilder::new_qml_module(QmlModule::new("com.sasedev.cam01").qml_files(["qml/main.qml"]))
        .qt_module("Qml")
        .qt_module("Network")
        .qt_module("Multimedia")
        .build();

    emit_runtime_hints(profile, &profile_name);
}
