1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
use log::{debug, error, info, warn};
use reqwest;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::Stdio;
use tempfile::NamedTempFile;
use tokio::fs as tokio_fs;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};

pub async fn download_fabric_binary() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let cache_dir = Path::new("./data");
    if let Err(e) = fs::create_dir_all(cache_dir) {
        error!("Failed to create cache directory: {e}");
        return Err(e.into());
    } else {
        debug!(
            "Cache directory created or already exists at {:?}",
            cache_dir
        );
    }
    // Determine the platform-specific binary name
    let platform_map = [
        ("windows", "fabric-windows-amd64.exe"),
        (
            "darwin",
            if cfg!(target_arch = "aarch64") {
                "fabric-darwin-arm64"
            } else {
                "fabric-darwin-amd64"
            },
        ),
        (
            "linux",
            if cfg!(target_arch = "aarch64") {
                "fabric-linux-arm64"
            } else {
                "fabric-linux-amd64"
            },
        ),
    ];

    let platform = match env::consts::OS {
        "windows" => "windows",
        "macos" | "darwin" => "darwin",
        "linux" => "linux",
        _ => {
            error!("Unsupported platform detected: {}", env::consts::OS);
            return Err("Unsupported platform".into());
        }
    };

    let binary_name_platform_specific = platform_map
        .iter()
        .find(|(p, _)| *p == platform)
        .ok_or_else(|| {
            error!("Failed to determine platform-specific binary name");
            "Unsupported platform"
        })?
        .1;

    // Check if the binary already exists in the cache directory
    let binary_path = cache_dir.join(binary_name_platform_specific);
    if binary_path.exists() {
        // If the binary already exists, return its path
        info!("Using cached binary at {:?}", binary_path);
        return Ok(binary_path);
    }

    let url = format!(
        "https://github.com/danielmiessler/fabric/releases/latest/download/{}",
        binary_name_platform_specific
    );
    info!("Downloading fabric binary from: {}", url);

    // Use a NamedTempFile to download the content into a temp file
    let temp_file = NamedTempFile::new()?;
    let temp_path = temp_file.path().to_path_buf();

    // let temp_dir = tempfile::tempdir()?;
    // let path = temp_dir.path().join(binary_name);

    // Perform the download with retries
    let response = reqwest::get(url).await?.error_for_status()?;
    let content = response.bytes().await?;
    let mut output_file = tokio_fs::File::create(&temp_path).await?;
    output_file.write_all(&content).await?;
    debug!("Downloaded binary to temporary path: {:?}", temp_path);

    // Move the temp file to the cache directory
    tokio_fs::rename(&temp_path, &binary_path).await?;
    info!("Binary moved to cache directory: {:?}", binary_path);

    // Make the binary executable on Unix-like systems
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        if let Ok(metadata) = fs::metadata(&binary_path) {
            let mut perms = metadata.permissions();
            perms.set_mode(0o755);
            if let Err(e) = fs::set_permissions(&binary_path, perms) {
                warn!("Failed to set executable permissions: {}", e);
            } else {
                debug!("Set executable permissions for {:?}", binary_path);
            }
        }
    }

    // Return the path to the downloaded binar
    Ok(binary_path)
}

pub async fn execute_fabric_command(args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
    let binary_path = download_fabric_binary().await?;
    info!("Executing fabric command with args: {:?}", args);

    let output = Command::new(binary_path)
        .args(args)
        .output()
        .expect("Failed to start fabric command");

    // Capture stderr and stdout as strings
    let stderr = String::from_utf8_lossy(&output.stderr);
    let stdout = String::from_utf8_lossy(&output.stdout);

    if !output.status.success() {
        error!("Fabric command failed with error: {} {}", stderr, stdout);
        // return Err(format!("Fabric command failed: {}", stderr).into());
    }

    // Log the command's standard output and return it
    info!("Fabric command output: {}", stdout);
    Ok(stdout.to_string())
}

pub async fn fabric_setup() -> Result<(String, String, bool), Box<dyn std::error::Error>> {
    // Define the path to the .env file
    let config_dir = env::var("HOME").unwrap_or_else(|_| ".".to_string());
    let env_path = PathBuf::from(format!("{}/.config/fabric/.env", config_dir));

    // Check if the .env file exists
    if env_path.exists() {
        info!("Found .env file at {:?}", env_path);

        // Read and parse the .env file
        return parse_env_file(&env_path).await;
    } else {
        // If the .env file does not exist, run `fabric --setup`
        info!(".env file not found, running `fabric --setup` to configure the application");

        // Execute the fabric --setup command interactively
        let binary_path = download_fabric_binary().await?;
        let mut cmd = tokio::process::Command::new(binary_path);
        cmd.arg("--setup");

        // Specify that we want the command's standard output piped back to us.
        // By default, standard input/output/error will be inherited from the
        // current process (for example, this means that standard input will
        // come from the keyboard and standard output/error will go directly to
        // the terminal if this process is invoked from the command line).
        cmd.stdout(Stdio::piped());

        let mut child = cmd.spawn().expect("failed to spawn command");

        let stdout = child
            .stdout
            .take()
            .expect("child did not have a handle to stdout");

        let mut reader = BufReader::new(stdout).lines();

        // Ensure the child process is spawned in the runtime so it can
        // make progress on its own while we await for any output.
        tokio::spawn(async move {
            let status = child
                .wait()
                .await
                .expect("child process encountered an error");

            println!("child status was: {}", status);
        });

        while let Some(line) = reader.next_line().await? {
            println!("{}", line);
        }
    }

    // After running setup, check again if the .env file was created
    if env_path.exists() {
        info!(".env file created successfully at {:?}", env_path);

        return parse_env_file(&env_path).await;
    } else {
        error!(".env file was not created after running `fabric --setup`");
        return Err("Setup did not create the .env file".into());
    }
}

async fn parse_env_file(
    env_path: &PathBuf,
) -> Result<(String, String, bool), Box<dyn std::error::Error>> {
    // Read the contents of the .env file
    let content = fs::read_to_string(env_path)?;
    let mut env_vars = HashMap::new();

    // Parse the .env content
    for line in content.lines() {
        if let Some((key, value)) = line.split_once('=') {
            env_vars.insert(key.trim().to_string(), value.trim().to_string());
        }
    }

    // Validate required fields
    let default_vendor = env_vars
        .get("DEFAULT_VENDOR")
        .ok_or("DEFAULT_VENDOR not found in .env file")?
        .clone();
    let default_model = env_vars
        .get("DEFAULT_MODEL")
        .ok_or("DEFAULT_MODEL not found in .env file")?
        .clone();
    let youtube_api_key_defined = env_vars.contains_key("YOUTUBE_API_KEY");

    debug!(
        "DEFAULT_VENDOR: {}, DEFAULT_MODEL: {}, YOUTUBE_API_KEY defined: {}",
        default_vendor, default_model, youtube_api_key_defined
    );

    // Return the values if all required fields are found
    Ok((default_vendor, default_model, youtube_api_key_defined))
}

pub async fn patterns() -> Result<String, Box<dyn std::error::Error>> {
    let args = vec!["--listpatterns"];
    let output = super::fabric::execute_fabric_command(&args).await?;

    Ok(output)
}

pub async fn models() -> Result<String, Box<dyn std::error::Error>> {
    let args = vec!["--listmodels"];
    let output = super::fabric::execute_fabric_command(&args).await?;

    Ok(output)
}

pub async fn change_default_model(model: &str) -> Result<String, Box<dyn std::error::Error>> {
    let args = vec!["--changeDefaultModel", model];
    let output = super::fabric::execute_fabric_command(&args).await?;

    Ok(output)
}

pub async fn scrape(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let args = vec!["--scrape_url", url];
    let output = super::fabric::execute_fabric_command(&args).await?;

    Ok(output)
}