1use crate::formats::parse_custom_format;
2use crate::transformers::custom_format_to_resource;
3
4use langcodec::{Codec, converter};
5use rayon::prelude::*;
6
7#[derive(Debug, Clone, PartialEq, clap::ValueEnum)]
9pub enum ConflictStrategy {
10    First,
12    Last,
14    Skip,
16}
17
18pub fn run_merge_command(
20    inputs: Vec<String>,
21    output: String,
22    strategy: ConflictStrategy,
23    lang: Option<String>,
24) {
25    if inputs.is_empty() {
26        eprintln!("Error: At least one input file is required.");
27        std::process::exit(1);
28    }
29
30    println!("Reading {} input files...", inputs.len());
32    let read_results: Vec<Result<Codec, String>> = inputs
33        .par_iter()
34        .map(|input| read_input_to_codec(input, lang.clone()))
35        .collect();
36
37    let mut input_codecs: Vec<Codec> = Vec::with_capacity(read_results.len());
38    for (idx, res) in read_results.into_iter().enumerate() {
39        match res {
40            Ok(c) => input_codecs.push(c),
41            Err(e) => {
42                println!("❌ Error reading input file {}/{}", idx + 1, inputs.len());
43                eprintln!("{}", e);
44                std::process::exit(1);
45            }
46        }
47    }
48
49    let mut codec = Codec::from_codecs(input_codecs);
51
52    println!("Merging resources...");
56    let conflict_strategy = match strategy {
57        ConflictStrategy::First => langcodec::types::ConflictStrategy::First,
58        ConflictStrategy::Last => langcodec::types::ConflictStrategy::Last,
59        ConflictStrategy::Skip => langcodec::types::ConflictStrategy::Skip,
60    };
61
62    let merge_count = codec.merge_resources(&conflict_strategy);
63    println!("Merged {} language groups", merge_count);
64
65    println!("Writing merged output...");
66    match converter::infer_format_from_path(output.clone()) {
67        Some(format) => {
68            println!("Converting resources to format: {:?}", format);
69            let source_language = codec
73                .resources
74                .first()
75                .and_then(|r| r.metadata.custom.get("source_language").cloned())
76                .unwrap_or_else(|| {
77                    codec
78                        .resources
79                        .first()
80                        .map(|r| r.metadata.language.clone())
81                        .unwrap_or("en".to_string())
82                });
83
84            println!("Setting metadata.source_language to: {}", source_language);
85
86            let version = codec
88                .resources
89                .first()
90                .and_then(|r| r.metadata.custom.get("version").cloned())
91                .unwrap_or_else(|| "1.0".to_string());
92
93            println!("Setting metadata.version to: {}", version);
94
95            codec.iter_mut().for_each(|r| {
96                r.metadata
97                    .custom
98                    .insert("source_language".to_string(), source_language.clone());
99                r.metadata
100                    .custom
101                    .insert("version".to_string(), version.clone());
102            });
103
104            if let Err(e) = converter::convert_resources_to_format(codec.resources, &output, format)
105            {
106                println!("❌ Error converting resources to format");
107                eprintln!("Error converting to {}: {}", output, e);
108                std::process::exit(1);
109            }
110        }
111        None => {
112            if codec.resources.len() == 1 {
113                println!("Writing single resource to output file");
114                if let Some(resource) = codec.resources.first()
115                    && let Err(e) = Codec::write_resource_to_file(resource, &output)
116                {
117                    println!("❌ Error writing output file");
118                    eprintln!("Error writing to {}: {}", output, e);
119                    std::process::exit(1);
120                }
121            } else {
122                println!("❌ Error writing output file");
123                eprintln!("Error writing to {}: multiple resources", output);
124                std::process::exit(1);
125            }
126        }
127    }
128
129    println!(
130        "✅ Successfully merged {} files into {}",
131        inputs.len(),
132        output
133    );
134}
135
136fn read_input_to_resources(
138    input: &str,
139    lang: Option<String>,
140) -> Result<Vec<langcodec::Resource>, String> {
141    {
143        let mut local_codec = Codec::new();
144        if let Ok(()) = local_codec.read_file_by_extension(input, lang.clone()) {
145            return Ok(local_codec.resources);
146        }
147    }
148
149    if input.ends_with(".json") || input.ends_with(".yaml") || input.ends_with(".yml") {
151        crate::validation::validate_custom_format_file(input)
153            .map_err(|e| format!("Failed to validate {}: {}", input, e))?;
154
155        let file_content = std::fs::read_to_string(input)
157            .map_err(|e| format!("Error reading file {}: {}", input, e))?;
158
159        crate::formats::validate_custom_format_content(input, &file_content)
161            .map_err(|e| format!("Invalid custom format {}: {}", input, e))?;
162
163        let resources = custom_format_to_resource(
165            input.to_string(),
166            parse_custom_format("json-language-map")
167                .map_err(|e| format!("Failed to parse custom format: {}", e))?,
168        )
169        .map_err(|e| format!("Failed to convert custom format {}: {}", input, e))?;
170
171        return Ok(resources);
172    }
173
174    Err(format!("Error reading {}: unsupported format", input))
175}
176
177fn read_input_to_codec(input: &str, lang: Option<String>) -> Result<Codec, String> {
179    let resources = read_input_to_resources(input, lang)?;
180    Ok(Codec { resources })
181}