Skip to main content

cargo_mate/captain/
license.rs

1use anyhow::{Context, Result};
2use reqwest::blocking;
3use serde::{Deserialize, Serialize};
4use std::env;
5use chrono::{DateTime, Utc};
6use std::fs;
7use std::path::PathBuf;
8use crate::license_guard::LicenseGuard;
9use crate::log::Log;
10#[cfg(unix)]
11use std::os::unix::fs::PermissionsExt;
12#[derive(Debug, Deserialize)]
13pub struct LicenseValidation {
14    pub valid: bool,
15    pub tier: String,
16    pub remaining: Option<i32>,
17    pub used: Option<i32>,
18    pub unlimited: Option<bool>,
19    pub expires_at: Option<String>,
20    pub error: Option<String>,
21}
22#[derive(Debug, Serialize)]
23pub struct LicenseRequest {
24    pub license_key: String,
25    pub command: String,
26    pub timestamp: DateTime<Utc>,
27    pub ip_address: Option<String>,
28    pub user_agent: Option<String>,
29}
30#[derive(Debug, Serialize)]
31pub struct LicenseCheckRequest {
32    pub license_key: String,
33    pub action: String,
34}
35pub struct LicenseManager {
36    api_base_url: String,
37}
38impl LicenseManager {
39    pub fn new() -> Result<Self> {
40        dotenvy::dotenv().ok();
41        let api_base_url = env::var("CARGO_MATE_API")
42            .unwrap_or_else(|_| "https://cargo.do/api".to_string());
43        Ok(Self { api_base_url })
44    }
45    pub fn register_license(&self, license_key: &str) -> Result<()> {
46        let log = Log::new();
47        log.log(
48            "Registering license",
49            vec!["license".to_string(), "register".to_string()],
50        )?;
51        log.log(
52            "Note: Licenses are now automatically linked to users when purchased.",
53            vec!["license".to_string(), "register".to_string()],
54        )?;
55        log.log(
56            "This command is for linking existing licenses to your current installation.",
57            vec!["license".to_string(), "register".to_string()],
58        )?;
59        if license_key.len() < 10 {
60            log.log(
61                "Invalid license format",
62                vec!["license".to_string(), "register".to_string()],
63            )?;
64            return Err(
65                anyhow::anyhow!("Invalid license format. Expected: <license_string>"),
66            );
67        }
68        let user_id = self.get_or_create_user_id()?;
69        log.log("User ID", vec!["license".to_string(), "register".to_string()])?;
70        let affiliate_code: Option<String> = None;
71        let client = blocking::Client::new();
72        let mut payload = serde_json::json!(
73            { "license_key" : license_key, "user_id" : user_id }
74        );
75        if let Some(afl_code) = affiliate_code {
76            payload["affiliate_code"] = serde_json::json!(afl_code);
77        }
78        let url = format!("{}/register-license", self.api_base_url);
79        log.log(
80            "Calling API endpoint",
81            vec!["license".to_string(), "register".to_string()],
82        )?;
83        let response = client
84            .post(&url)
85            .json(&payload)
86            .send()
87            .context("Failed to connect to license registration API")?;
88        if !response.status().is_success() {
89            let status = response.status();
90            let error_text = response
91                .text()
92                .unwrap_or_else(|_| "Unknown error".to_string());
93            log.log(
94                "Registration failed",
95                vec!["license".to_string(), "register".to_string()],
96            )?;
97            crate::wtf::display_api_failure_art();
98            std::process::exit(1);
99        }
100        let registration_result: serde_json::Value = response
101            .json()
102            .context("Failed to parse license registration response")?;
103        if registration_result["success"] != true {
104            let error_msg = registration_result["error"]
105                .as_str()
106                .unwrap_or("Unknown registration error");
107            return Err(anyhow::anyhow!("License registration failed: {}", error_msg));
108        }
109        let tier = registration_result["tier"].as_str().unwrap_or("FREE").to_string();
110        self.save_local_license(license_key, &tier)?;
111        log.log(
112            "License registered successfully",
113            vec!["license".to_string(), "register".to_string()],
114        )?;
115        log.log("Tier", vec!["license".to_string(), "register".to_string()])?;
116        log.log("User", vec!["license".to_string(), "register".to_string()])?;
117        let obfuscated_license = LicenseGuard::obfuscate_license(license_key);
118        log.log(
119            "Obfuscated license",
120            vec!["license".to_string(), "register".to_string()],
121        )?;
122        if let Err(e) = LicenseGuard::store_license(&obfuscated_license) {
123            log.log(
124                "Error storing license",
125                vec!["license".to_string(), "register".to_string()],
126            )?;
127        }
128        log.log("License stored", vec!["license".to_string(), "register".to_string()])?;
129        Ok(())
130    }
131    pub fn show_user_info(&self) -> Result<()> {
132        let log = Log::new();
133        let user_id = self.get_or_create_user_id()?;
134        log.log("User Information", vec!["license".to_string(), "show".to_string()])?;
135        log.log("User ID", vec!["license".to_string(), "show".to_string()])?;
136        let license_key = match LicenseGuard::retrieve_license() {
137            Ok(Some(license)) => license,
138            Ok(None) => {
139                log.log(
140                    "License not found",
141                    vec!["license".to_string(), "show".to_string()],
142                )?;
143                return Ok(());
144            }
145            Err(_) => {
146                log.log(
147                    "Failed to retrieve license",
148                    vec!["license".to_string(), "show".to_string()],
149                )?;
150                return Ok(());
151            }
152        };
153        log.log("License", vec!["license".to_string(), "show".to_string()])?;
154        match self.check_license_status() {
155            Ok(validation) => {
156                log.log("Tier", vec!["license".to_string(), "show".to_string()])?;
157                if let Some(expires_at) = validation.expires_at {
158                    log.log("Expires", vec!["license".to_string(), "show".to_string()])?;
159                }
160                if validation.tier == "FREE" {
161                    if let Some(remaining) = validation.remaining {
162                        log.log(
163                            "Remaining commands",
164                            vec!["license".to_string(), "show".to_string()],
165                        )?;
166                    }
167                }
168            }
169            Err(e) => {
170                log.log(
171                    "License status check failed",
172                    vec!["license".to_string(), "show".to_string()],
173                )?;
174            }
175        }
176        Ok(())
177    }
178    pub fn check_license_status(&self) -> Result<LicenseValidation> {
179        let license_key = self.get_local_license()?;
180        let user_id = self.get_or_create_user_id()?;
181        let client = blocking::Client::new();
182        let response = client
183            .post(&format!("{}/licenses/validate", self.api_base_url))
184            .json(
185                &serde_json::json!(
186                    { "license_key" : license_key, "user_id" : user_id, "action" :
187                    "check_status" }
188                ),
189            )
190            .send()
191            .context("Failed to connect to license API")?;
192        if response.status().is_success() {
193            let validation: LicenseValidation = response
194                .json()
195                .context("Failed to parse license validation response")?;
196            Ok(validation)
197        } else {
198            Ok(LicenseValidation {
199                valid: false,
200                tier: "FREE".to_string(),
201                remaining: Some(0),
202                used: None,
203                unlimited: Some(false),
204                expires_at: None,
205                error: Some("License not found or inactive".to_string()),
206            })
207        }
208    }
209    pub fn record_usage(&self, command: &str) -> Result<()> {
210        let log = Log::new();
211        let license_key = self.get_local_license()?;
212        let user_id = self.get_or_create_user_id()?;
213        let client = blocking::Client::new();
214        let response = client
215            .post(&format!("{}/licenses/validate", self.api_base_url))
216            .json(
217                &serde_json::json!(
218                    { "license_key" : license_key, "user_id" : user_id, "action" :
219                    "use_command" }
220                ),
221            )
222            .timeout(std::time::Duration::from_secs(3))
223            .send();
224        match response {
225            Ok(resp) if resp.status().is_success() => Ok(()),
226            Ok(resp) => {
227                let status_code = resp.status();
228                log.log(
229                    "Warning: Failed to record command usage (HTTP {})",
230                    vec!["license".to_string(), "record_usage".to_string()],
231                )?;
232                log.log(
233                    "Your command was executed but usage may not be properly tracked.",
234                    vec!["license".to_string(), "record_usage".to_string()],
235                )?;
236                Ok(())
237            }
238            Err(e) => {
239                log.log(
240                    "Warning: Could not connect to license server to record usage",
241                    vec!["license".to_string(), "record_usage".to_string()],
242                )?;
243                log.log(
244                    "Your command was executed but usage may not be properly tracked.",
245                    vec!["license".to_string(), "record_usage".to_string()],
246                )?;
247                eprintln!(
248                    "⚠️  Your command was executed but usage may not be properly tracked."
249                );
250                Ok(())
251            }
252        }
253    }
254    pub fn enforce_license(&self, command: &str) -> Result<()> {
255        let log = Log::new();
256        let user_id = self.get_or_create_user_id()?;
257        let license_key = match LicenseGuard::retrieve_license() {
258            Ok(key) => key,
259            Err(_) => {
260                if let Ok(Some(key)) = LicenseGuard::retrieve_license() {
261                    let home_dir = dirs::home_dir().unwrap();
262                    let config_dir = home_dir.join(".shipwreck");
263                    let license_file = config_dir.join("license.key");
264                    let _ = fs::write(&license_file, &key);
265                    Some(key)
266                } else {
267                    eprintln!(
268                        "┌─────────────────────────────────────────────────────┐"
269                    );
270                    eprintln!(
271                        "│  ❌ FATAL: No valid license found                  │"
272                    );
273                    eprintln!(
274                        "├─────────────────────────────────────────────────────┤"
275                    );
276                    eprintln!(
277                        "│  Cargo Mate requires a license to operate.         │"
278                    );
279                    eprintln!(
280                        "│  Auto-registration may have failed.                │"
281                    );
282                    eprintln!(
283                        "│                                                     │"
284                    );
285                    eprintln!(
286                        "│  Please reinstall to get your FREE license:        │"
287                    );
288                    eprintln!(
289                        "│  curl -sSL https://get.cargo.do/mate | bash       │"
290                    );
291                    eprintln!(
292                        "└─────────────────────────────────────────────────────┘"
293                    );
294                    std::process::exit(1);
295                }
296            }
297        };
298        let validation = match self.check_license_status() {
299            Ok(validation) => validation,
300            Err(_) => {
301                match LicenseGuard::get_stored_license_info() {
302                    Ok(Some(_)) => {
303                        LicenseValidation {
304                            valid: true,
305                            tier: "PRO".to_string(),
306                            remaining: None,
307                            used: None,
308                            unlimited: Some(true),
309                            expires_at: None,
310                            error: None,
311                        }
312                    }
313                    _ => {
314                        log.log(
315                            "No internet connection. FREE tier requires online verification.",
316                            vec!["license".to_string(), "enforce_license".to_string()],
317                        )?;
318                        std::process::exit(1);
319                    }
320                }
321            }
322        };
323        if !validation.valid {
324            if let Some(error) = validation.error {
325                log.log(
326                    "License validation failed",
327                    vec!["license".to_string(), "enforce_license".to_string()],
328                )?;
329            }
330            log.log(
331                "Your license key",
332                vec!["license".to_string(), "enforce_license".to_string()],
333            )?;
334            std::process::exit(1);
335        }
336        if validation.tier == "FREE" {
337            let api_remaining = validation.remaining.unwrap_or(0);
338            let local_count = self.get_local_command_count()?;
339            let effective_remaining = if api_remaining <= local_count {
340                api_remaining
341            } else {
342                10 - local_count
343            };
344            if effective_remaining <= 0 {
345                eprintln!(
346                    "┌─────────────────────────────────────────────────────┐"
347                );
348                eprintln!("│  :( DAILY LIMIT EXCEEDED 0/10 commands ):           │");
349                eprintln!(
350                    "├─────────────────────────────────────────────────────┤"
351                );
352                eprintln!("│  FREE tier limit exhausted for today.               │");
353                eprintln!("│                                                     │");
354                eprintln!("│  Options:                                           │");
355                eprintln!(
356                    "│  • Wait until midnight UTC for reset                │"
357                );
358                eprintln!(
359                    "│  • Upgrade to PRO: https://cargo.do/pro             │"
360                );
361                eprintln!(
362                    "└─────────────────────────────────────────────────────┘"
363                );
364                std::process::exit(1);
365            }
366        }
367        self.record_usage(command)?;
368        if validation.tier == "FREE" {
369            match self.increment_local_command_count() {
370                Ok(_) => {}
371                Err(e) => {}
372            }
373        }
374        Ok(())
375    }
376    pub fn get_or_create_user_id(&self) -> Result<String> {
377        let log = Log::new();
378        let home_dir = dirs::home_dir().context("Could not find home directory")?;
379        let config_dir = home_dir.join(".shipwreck");
380        let user_id_file = config_dir.join("user.id");
381        if user_id_file.exists() {
382            let user_id = fs::read_to_string(&user_id_file)
383                .context("Failed to read user ID file")?
384                .trim()
385                .to_string();
386            Ok(user_id)
387        } else {
388            fs::create_dir_all(&config_dir)
389                .context("Failed to create config directory")?;
390            let user_id = format!(
391                "cm_{}_{}", Utc::now().timestamp(), std::process::id()
392            );
393            let client = blocking::Client::new();
394            let payload = serde_json::json!({ "user_id" : user_id.clone() });
395            match client
396                .post(&format!("{}/users/register", self.api_base_url))
397                .json(&payload)
398                .timeout(std::time::Duration::from_secs(5))
399                .send()
400            {
401                Ok(response) if response.status().is_success() => {
402                    if let Ok(json) = response.json::<serde_json::Value>() {
403                        fs::write(&user_id_file, &user_id)
404                            .context("Failed to save user ID file")?;
405                        if let Some(license_key) = json["license_key"].as_str() {
406                            let license_file = config_dir.join("license.key");
407                            fs::write(&license_file, license_key)
408                                .context("Failed to save license file")?;
409                            if let Some(tier) = json["license_tier"].as_str() {
410                                let tier_file = config_dir.join("license.tier");
411                                fs::write(&tier_file, tier)
412                                    .context("Failed to save tier file")?;
413                            }
414                            if let Err(e) = crate::license_guard::LicenseGuard::store_license(
415                                license_key,
416                            ) {
417                                log.log(
418                                    "Warning: Could not store license in hidden locations",
419                                    vec!["license".to_string(), "register".to_string()],
420                                )?;
421                            }
422                            log.log(
423                                "User registered successfully",
424                                vec!["license".to_string(), "register".to_string()],
425                            )?;
426                            log.log(
427                                "User ID",
428                                vec!["license".to_string(), "register".to_string()],
429                            )?;
430                            log.log(
431                                "License",
432                                vec!["license".to_string(), "register".to_string()],
433                            )?;
434                            log.log(
435                                "Affiliate program info",
436                                vec!["license".to_string(), "register".to_string()],
437                            )?;
438                            let result: Result<(), anyhow::Error> = Ok(());
439                            if let Err(e) = result {
440                                log.log(
441                                    "Warning: Could not show affiliate info",
442                                    vec!["license".to_string(), "register".to_string()],
443                                )?;
444                            }
445                            if let Some(afl_code) = json["affiliate_code"].as_str() {
446                                let afl_file = config_dir.join("affiliate_code");
447                                let _ = fs::write(&afl_file, afl_code);
448                                log.log(
449                                    "Affiliate Code",
450                                    vec!["license".to_string(), "register".to_string()],
451                                )?;
452                            }
453                        }
454                    }
455                }
456                _ => {
457                    fs::write(&user_id_file, &user_id)
458                        .context("Failed to save user ID file")?;
459                    log.log(
460                        "Could not connect to license server. Running in offline mode",
461                        vec!["license".to_string(), "register".to_string()],
462                    )?;
463                }
464            }
465            Ok(user_id)
466        }
467    }
468    fn get_ip_address(&self) -> String {
469        match self.fetch_external_ip() {
470            Ok(ip) => ip,
471            Err(_) => {
472                std::env::var("SSH_CLIENT")
473                    .or_else(|_| std::env::var("REMOTE_ADDR"))
474                    .unwrap_or_else(|_| "127.0.0.1".to_string())
475                    .split_whitespace()
476                    .next()
477                    .unwrap_or("127.0.0.1")
478                    .to_string()
479            }
480        }
481    }
482    fn fetch_external_ip(&self) -> Result<String> {
483        let client = blocking::Client::new();
484        let response = client
485            .get("https://api.ipify.org")
486            .timeout(std::time::Duration::from_secs(2))
487            .send()
488            .context("Failed to fetch external IP")?;
489        if response.status().is_success() {
490            let ip = response
491                .text()
492                .context("Failed to read IP response")?
493                .trim()
494                .to_string();
495            Ok(ip)
496        } else {
497            Err(anyhow::anyhow!("Failed to get external IP"))
498        }
499    }
500    pub fn get_local_license(&self) -> Result<String> {
501        let log = Log::new();
502        let home_dir = dirs::home_dir().context("Could not find home directory")?;
503        let config_dir = home_dir.join(".shipwreck");
504        let license_file = config_dir.join("license.key");
505        if !license_file.exists() {
506            log.log(
507                "No license found. Run 'cm register <license-key>' to register your Pro license",
508                vec!["license".to_string(), "get_local_license".to_string()],
509            )?;
510            return Err(
511                anyhow::anyhow!(
512                    "No license found. Run 'cm register <license-key>' to register your Pro license"
513                ),
514            );
515        }
516        let license = fs::read_to_string(license_file)
517            .context("Failed to read license file")?
518            .trim()
519            .to_string();
520        Ok(license)
521    }
522    fn save_local_license(&self, license_key: &str, tier: &str) -> Result<()> {
523        let log = Log::new();
524        let home_dir = dirs::home_dir().context("Could not find home directory")?;
525        let config_dir = home_dir.join(".shipwreck");
526        fs::create_dir_all(&config_dir).context("Failed to create config directory")?;
527        let license_file = config_dir.join("license.key");
528        fs::write(&license_file, license_key).context("Failed to save license file")?;
529        let tier_file = config_dir.join("license.tier");
530        fs::write(&tier_file, tier).context("Failed to save license tier")?;
531        let obfuscated_license = LicenseGuard::obfuscate_license(license_key);
532        if let Err(e) = LicenseGuard::store_license(&obfuscated_license) {
533            log.log(
534                "Warning: Could not store license in hidden locations",
535                vec!["license".to_string(), "save_local_license".to_string()],
536            )?;
537        }
538        log.log(
539            "License saved",
540            vec!["license".to_string(), "save_local_license".to_string()],
541        )?;
542        Ok(())
543    }
544    pub fn get_stored_license_info(&self) -> Result<(String, String)> {
545        let log = Log::new();
546        let license_key = self.get_local_license()?;
547        let home_dir = dirs::home_dir().unwrap();
548        let config_dir = home_dir.join(".shipwreck");
549        let tier_file = config_dir.join("license.tier");
550        let tier = if tier_file.exists() {
551            fs::read_to_string(tier_file)?.trim().to_string()
552        } else {
553            "FREE".to_string()
554        };
555        Ok((license_key, tier))
556    }
557    pub fn get_license_info(&self) -> Result<serde_json::Value> {
558        let license_key = self.get_local_license()?;
559        let client = blocking::Client::new();
560        let response = client
561            .post(&format!("{}/licenses/info", self.api_base_url))
562            .json(&serde_json::json!({ "license_key" : license_key }))
563            .send()
564            .context("Failed to connect to license API")?;
565        if response.status().is_success() {
566            let info: serde_json::Value = response
567                .json()
568                .context("Failed to parse license info response")?;
569            Ok(info)
570        } else {
571            Err(anyhow::anyhow!("Failed to get license info: {}", response.status()))
572        }
573    }
574    pub fn check_remaining_commands(&self) -> Result<i32> {
575        let locations = Self::get_counter_locations();
576        let mut incremented = false;
577        for (base_path, filename) in &locations {
578            if let Ok(path) = self.resolve_counter_path(base_path, filename) {
579                let current_count = if path.exists() {
580                    if let Ok(content) = fs::read_to_string(&path) {
581                        if let Ok(count) = self.decrypt_counter(&content) {
582                            count
583                        } else {
584                            0
585                        }
586                    } else {
587                        0
588                    }
589                } else {
590                    0
591                };
592                let new_count = current_count + 1;
593                let encrypted = self.encrypt_counter(new_count)?;
594                if fs::write(&path, &encrypted).is_ok() {
595                    incremented = true;
596                }
597            }
598        }
599        let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
600        let config_dir = home_dir.join(".shipwreck");
601        let old_counter_file = config_dir.join("daily_commands");
602        let old_count = if old_counter_file.exists() {
603            if let Ok(content) = fs::read_to_string(&old_counter_file) {
604                content.trim().parse::<i32>().unwrap_or(0)
605            } else {
606                0
607            }
608        } else {
609            0
610        };
611        let _ = fs::write(&old_counter_file, format!("{}", old_count + 1));
612        let validation = self.check_license_status()?;
613        if !validation.valid {
614            return Err(
615                anyhow::anyhow!(
616                    validation.error.unwrap_or_else(|| "License validation failed"
617                    .to_string())
618                ),
619            );
620        }
621        match validation.tier.as_str() {
622            "PRO" => Ok(-1),
623            "FREE" => Ok(validation.remaining.unwrap_or(0)),
624            _ => Ok(0),
625        }
626    }
627    pub fn is_license_expired(&self) -> Result<bool> {
628        let license_key = self.get_local_license()?;
629        let client = blocking::Client::new();
630        let response = client
631            .post(&format!("{}/licenses/check_expiration", self.api_base_url))
632            .json(&serde_json::json!({ "license_key" : license_key }))
633            .send()
634            .context("Failed to connect to license API")?;
635        if response.status().is_success() {
636            let result: serde_json::Value = response
637                .json()
638                .context("Failed to parse expiration check response")?;
639            Ok(result["expired"].as_bool().unwrap_or(true))
640        } else {
641            Ok(true)
642        }
643    }
644    fn get_counter_locations() -> Vec<(String, String)> {
645        let mut locations = Vec::new();
646        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
647        let os_type = std::env::consts::OS;
648        let is_windows = os_type == "windows";
649        let is_macos = os_type == "macos";
650        if let Ok(cargo_home) = std::env::var("CARGO_HOME") {
651            locations.push((format!("{}/bin", cargo_home), ".cmd_count".to_string()));
652        } else {
653            if is_windows {
654                locations
655                    .push((
656                        "%USERPROFILE%\\.cargo\\bin".to_string(),
657                        ".cmd_count".to_string(),
658                    ));
659            } else {
660                locations.push(("~/.cargo/bin".to_string(), ".cmd_count".to_string()));
661            }
662        }
663        if is_windows {
664            locations.push(("%APPDATA%".to_string(), ".cargo_count".to_string()));
665        } else if is_macos {
666            locations
667                .push(("~/Library/Preferences".to_string(), ".cargo_count".to_string()));
668            locations
669                .push((
670                    "~/Library/Application Support/cargo-mate".to_string(),
671                    ".cargo_count".to_string(),
672                ));
673            if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
674                locations
675                    .push((
676                        format!("{}/cargo-mate", xdg_config),
677                        ".cargo_count".to_string(),
678                    ));
679            }
680        } else {
681            if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
682                locations
683                    .push((
684                        format!("{}/cargo-mate", xdg_config),
685                        ".cargo_count".to_string(),
686                    ));
687            } else {
688                locations
689                    .push((
690                        "~/.config/cargo-mate".to_string(),
691                        ".cargo_count".to_string(),
692                    ));
693                locations
694                    .push(("~/.cargo-mate".to_string(), ".cargo_count".to_string()));
695            }
696        }
697        if is_windows {
698            locations.push(("%LOCALAPPDATA%".to_string(), ".build_count".to_string()));
699        } else if is_macos {
700            locations
701                .push((
702                    "~/Library/Application Support/cargo-mate".to_string(),
703                    ".build_count".to_string(),
704                ));
705            locations
706                .push((
707                    "~/Library/Preferences/cargo-mate".to_string(),
708                    ".build_count".to_string(),
709                ));
710            if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") {
711                locations
712                    .push((
713                        format!("{}/cargo-mate", xdg_data),
714                        ".build_count".to_string(),
715                    ));
716            }
717        } else {
718            if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") {
719                locations
720                    .push((
721                        format!("{}/cargo-mate", xdg_data),
722                        ".build_count".to_string(),
723                    ));
724            } else {
725                locations
726                    .push((
727                        "~/.local/share/cargo-mate".to_string(),
728                        ".build_count".to_string(),
729                    ));
730                locations
731                    .push(("~/.cargo-mate".to_string(), ".build_count".to_string()));
732                locations
733                    .push((
734                        "~/.local/cargo-mate".to_string(),
735                        ".build_count".to_string(),
736                    ));
737            }
738        }
739        if is_windows {
740            locations.push(("%TEMP%".to_string(), ".cm_usage".to_string()));
741        } else if is_macos {
742            locations
743                .push((
744                    "~/Library/Caches/cargo-mate".to_string(),
745                    ".cm_usage".to_string(),
746                ));
747            locations
748                .push((
749                    "~/Library/Application Support/cargo-mate/cache".to_string(),
750                    ".cm_usage".to_string(),
751                ));
752            if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
753                locations
754                    .push((
755                        format!("{}/cargo-mate", xdg_cache),
756                        ".cm_usage".to_string(),
757                    ));
758            }
759        } else {
760            if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
761                locations
762                    .push((
763                        format!("{}/cargo-mate", xdg_cache),
764                        ".cm_usage".to_string(),
765                    ));
766            } else {
767                locations
768                    .push(("~/.cache/cargo-mate".to_string(), ".cm_usage".to_string()));
769                locations
770                    .push(("~/.cargo-mate/cache".to_string(), ".cm_usage".to_string()));
771                locations.push(("/tmp/cargo-mate".to_string(), ".cm_usage".to_string()));
772            }
773        }
774        if let Ok(temp_dir) = std::env::var("TMPDIR") {
775            locations.push((temp_dir, ".daily_limit".to_string()));
776        } else if let Ok(temp_dir) = std::env::var("TEMP") {
777            locations.push((temp_dir, ".daily_limit".to_string()));
778        } else {
779            locations.push(("/tmp".to_string(), ".daily_limit".to_string()));
780        }
781        locations
782    }
783    fn get_local_command_count(&self) -> Result<i32> {
784        let mut max_count = 0;
785        let mut found_any = false;
786        let locations = Self::get_counter_locations();
787        for (base_path, filename) in &locations {
788            if let Ok(path) = self.resolve_counter_path(&base_path, &filename) {
789                if path.exists() {
790                    if let Ok(content) = fs::read_to_string(&path) {
791                        if let Ok(decrypted) = self.decrypt_counter(&content) {
792                            max_count = max_count.max(decrypted);
793                            found_any = true;
794                        }
795                    }
796                }
797            }
798        }
799        if found_any { Ok(max_count) } else { Ok(0) }
800    }
801    fn increment_local_command_count(&self) -> Result<()> {
802        let current_count = self.get_local_command_count()?;
803        let new_count = current_count + 1;
804        let encrypted_count = self.encrypt_counter(new_count)?;
805        let locations = Self::get_counter_locations();
806        let mut successful_stores = 0;
807        for (base_path, filename) in &locations {
808            if let Ok(path) = self.resolve_counter_path(&base_path, &filename) {
809                if let Some(parent) = path.parent() {
810                    let _ = fs::create_dir_all(parent);
811                }
812                if fs::write(&path, &encrypted_count).is_ok() {
813                    #[cfg(unix)]
814                    {
815                        if let Ok(metadata) = fs::metadata(&path) {
816                            let mut perms = metadata.permissions();
817                            perms.set_mode(0o400);
818                            let _ = fs::set_permissions(&path, perms);
819                        }
820                    }
821                    successful_stores += 1;
822                }
823            }
824        }
825        if successful_stores == 0 {
826            return Err(
827                anyhow::anyhow!("Failed to store command counter in any location"),
828            );
829        }
830        Ok(())
831    }
832    fn resolve_counter_path(&self, base_path: &str, filename: &str) -> Result<PathBuf> {
833        let expanded = if base_path.starts_with("~/") {
834            let home = dirs::home_dir().context("Could not find home directory")?;
835            home.join(&base_path[2..]).join(filename)
836        } else if base_path.starts_with("%") && base_path.ends_with("%") {
837            let env_var = &base_path[1..base_path.len() - 1];
838            if let Ok(env_path) = std::env::var(env_var) {
839                PathBuf::from(env_path).join(filename)
840            } else {
841                PathBuf::from(base_path).join(filename)
842            }
843        } else {
844            PathBuf::from(base_path).join(filename)
845        };
846        Ok(expanded)
847    }
848    fn encrypt_counter(&self, count: i32) -> Result<String> {
849        let salt = 0x1337;
850        let obfuscated = ((count as u32) ^ salt).to_string();
851        Ok(obfuscated.chars().rev().collect())
852    }
853    fn decrypt_counter(&self, encrypted: &str) -> Result<i32> {
854        let reversed: String = encrypted.chars().rev().collect();
855        let obfuscated: u32 = reversed.parse()?;
856        let salt = 0x1337;
857        let count = (obfuscated ^ salt) as i32;
858        Ok(count)
859    }
860    pub fn reset_local_command_count(&self) -> Result<()> {
861        let encrypted_zero = self.encrypt_counter(0)?;
862        let locations = Self::get_counter_locations();
863        for (base_path, filename) in &locations {
864            if let Ok(path) = self.resolve_counter_path(base_path, filename) {
865                if path.exists() {
866                    let _ = fs::write(&path, &encrypted_zero);
867                }
868            }
869        }
870        let home_dir = dirs::home_dir().context("Could not find home directory")?;
871        let config_dir = home_dir.join(".shipwreck");
872        let old_counter_file = config_dir.join("daily_commands");
873        if old_counter_file.exists() {
874            let _ = fs::write(&old_counter_file, "0");
875        }
876        Ok(())
877    }
878    pub fn debug_command_counters(&self) -> Result<()> {
879        let log = Log::new();
880        let local_count = self.get_local_command_count()?;
881        let locations = Self::get_counter_locations();
882        log.log(
883            "🔍 Command Counter Debug:",
884            vec!["license".to_string(), "debug_command_counters".to_string()],
885        )?;
886        log.log(
887            "   📁 Local counter",
888            vec!["license".to_string(), "debug_command_counters".to_string()],
889        )?;
890        log.log(
891            "   🖥️  Platform",
892            vec!["license".to_string(), "debug_command_counters".to_string()],
893        )?;
894        log.log(
895            "   🔐 Dynamic locations",
896            vec!["license".to_string(), "debug_command_counters".to_string()],
897        )?;
898        for (base_path, filename) in &locations {
899            if let Ok(path) = self.resolve_counter_path(base_path, filename) {
900                let status = if path.exists() {
901                    if let Ok(content) = fs::read_to_string(&path) {
902                        if let Ok(count) = self.decrypt_counter(&content) {
903                            format!("✅ {} commands", count)
904                        } else {
905                            "❌ Corrupted".to_string()
906                        }
907                    } else {
908                        "❌ Unreadable".to_string()
909                    }
910                } else {
911                    "❌ Missing".to_string()
912                };
913                let display_path = if base_path.contains("~") {
914                    format!("~{}", & base_path[1..])
915                } else {
916                    base_path.to_string()
917                };
918                println!("      {} -> {}: {}", filename, display_path, status);
919            } else {
920                println!("      {} -> {}: ❌ Invalid path", filename, base_path);
921            }
922        }
923        match self.check_license_status() {
924            Ok(validation) => {
925                if validation.tier == "FREE" {
926                    let api_remaining = validation.remaining.unwrap_or(0);
927                    let api_used = validation.used.unwrap_or(0);
928                    println!("   🗄️  API remaining: {} commands", api_remaining);
929                    println!("   📊 API used: {} commands", api_used);
930                    println!(
931                        "   📈 Total API limit: {} commands", api_used + api_remaining
932                    );
933                } else {
934                    println!("   🌟 PRO tier: Unlimited commands");
935                }
936            }
937            Err(e) => {
938                println!("   ❌ API counter: Unavailable ({})", e);
939            }
940        }
941        Ok(())
942    }
943    pub fn get_user_tier(&self) -> Result<String> {
944        let user_id = self.get_or_create_user_id()?;
945        let home_dir = dirs::home_dir().context("Could not find home directory")?;
946        let config_dir = home_dir.join(".shipwreck");
947        let tier_file = config_dir.join("license.tier");
948        if tier_file.exists() {
949            let tier = fs::read_to_string(&tier_file)?.trim().to_string();
950            if !tier.is_empty() {
951                return Ok(tier);
952            }
953        }
954        Ok("FREE".to_string())
955    }
956    pub fn get_remaining_commands(&self) -> Result<i32> {
957        let validation = self.check_license_status()?;
958        if validation.tier == "FREE" {
959            let api_remaining = validation.remaining.unwrap_or(0);
960            let local_count = self.get_local_command_count()?;
961            let effective_remaining = if api_remaining <= local_count {
962                api_remaining
963            } else {
964                10 - local_count
965            };
966            Ok(effective_remaining.max(0))
967        } else {
968            Ok(i32::MAX)
969        }
970    }
971}