use std::io::Write;
use std::fs::File;
use std::path::Path;
use std::process::Command;
use std::sync::mpsc::channel;
use std::thread;
// use git2::*;
use super::worker::Worker;
use time_0_3::OffsetDateTime;

#[derive(Clone, Debug)]
pub struct Options {
    pub threads:   u32,
    pub target:    String,
    pub message:   Vec<String>,
    pub repo:      String,
    pub timestamp: OffsetDateTime,
    pub kind:      Option<u16>,
}

pub struct Gitminer {
    opts:   Options,
    repo:   git2::Repository,
    author: String
}

impl std::fmt::Debug for Gitminer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Gitminer")
            .field("opts", &self.opts)
            .field("repo_path", &self.repo.path())
            .field("author", &self.author)
            .finish()
    }
}

impl Clone for Gitminer {
    fn clone(&self) -> Self {
        let repo = git2::Repository::open(&self.opts.repo)
            .expect("Failed to open repository during clone");
        Self {
            opts: self.opts.clone(),
            repo,
            author: self.author.clone(),
        }
    }
}


impl Gitminer {
    pub fn new(opts: Options) -> Result<Gitminer, &'static str> {

        let repo = match git2::Repository::open(&opts.repo) {
            Ok(r)  => r,
            Err(e) => {
                error!("Failed to open repository: {}", e);
                return Err("Failed to open repository");
            }
        };

        let author = Gitminer::load_author(&repo)?;
        debug!("Gitminer initialized with author: {}", author);

        Ok(Gitminer {
            opts:   opts,
            repo:   repo,
            author: author
        })
    }

    pub fn mine(&mut self) -> Result<String, &'static str> {
        debug!("Starting mining process with options: {:?}", self.opts);
        let (tree, parent) = match Gitminer::prepare_tree(&mut self.repo) {
            Ok((t, p)) => (t, p),
            Err(e)   => {
                error!("Failed to prepare tree: {}", e);
                return Err(e);
            }
        };
        debug!("Tree: {}, Parent: {}", tree, parent);


        let (tx, rx) = channel();

        for i in 0..self.opts.threads {
            let target = self.opts.target.clone();
            let author = self.author.clone();
            let msg = if self.opts.message.len() > 1 {
                format!("{}

{}", self.opts.message[0], self.opts.message[1..].join("\n"))
            } else {
                self.opts.message[0].clone()
            };
            let wtx    = tx.clone();
            let ts     = self.opts.timestamp.clone();
            let (wtree, wparent) = (tree.clone(), parent.clone());

            debug!("Spawning worker {}", i);
            thread::spawn(move || {
                Worker::new(i, target, wtree, wparent, author, msg, ts, wtx).work();
            });
        }

        let (_, blob, hash) = rx.recv().unwrap();
        info!("Received hash {} from a worker.", hash);

        match self.write_commit(&hash, &blob) {
            Ok(_)  => {
                print!("Mined commit hash: {}", hash);
                Ok(hash)
            },
            Err(e) => {
                error!("Failed to write commit: {}", e);
                Err(e)
            }
        }
    }

    fn write_commit(&self, hash: &String, blob: &String) -> Result<(), &'static str> {
        Gitminer::ensure_gnostr_dirs_exist(Path::new(&self.opts.repo))?;
        debug!("Writing commit for hash: {}", hash);
        /* repo.blob() generates a blob, not a commit.
         * don't know if there's a way to do this with libgit2. */
        let tmpfile  = format!("/tmp/{}.tmp", hash);
        debug!("Creating temporary file: {}", tmpfile);
        let mut file = match File::create(&Path::new(&tmpfile)) {
            Ok(f) => f,
            Err(_) => return Err("Failed to create temporary file"),
        };

        if let Err(_) = file.write_all(blob.as_bytes()) {
            return Err("Failed to write to temporary file");
        }
        debug!("Temporary file {} written.", tmpfile);

        // Write the blob to .gnostr/blobs/<commit_hash>
        let gnostr_blob_path = Path::new(&self.opts.repo).join(".gnostr/blobs").join(hash);
        debug!("Creating .gnostr blob file: {}", gnostr_blob_path.display());
        let mut gnostr_blob_file = match File::create(&gnostr_blob_path) {
            Ok(f) => f,
            Err(_) => return Err("Failed to create .gnostr blob file"),
        };
        if let Err(_) = gnostr_blob_file.write_all(blob.as_bytes()) {
            return Err("Failed to write to .gnostr blob file");
        }
        debug!(".gnostr blob file {} written.", gnostr_blob_path.display());

        // Write the blob to .gnostr/reflog/<commit_hash>
        let gnostr_reflog_path = Path::new(&self.opts.repo).join(".gnostr/reflog").join(hash);
        debug!("Creating .gnostr reflog file: {}", gnostr_reflog_path.display());
        let mut gnostr_reflog_file = match File::create(&gnostr_reflog_path) {
            Ok(f) => f,
            Err(_) => return Err("Failed to create .gnostr reflog file"),
        };
        if let Err(_) = gnostr_reflog_file.write_all(blob.as_bytes()) {
            return Err("Failed to write to .gnostr reflog file");
        }
        debug!(".gnostr reflog file {} written.", gnostr_reflog_path.display());

        let command_str = format!("cd {} && git hash-object -t commit -w --stdin < {} && git reset --hard {}", self.opts.repo, tmpfile, hash);
        debug!("Executing git command: {}", command_str);
        let output = Command::new("sh")
            .arg("-c")
            .arg(&command_str)
            .output()
            .map_err(|_| "Failed to execute git command")?;

        if !output.status.success() {
            eprintln!("Failed git command: {}", command_str);
            error!("Git command failed: {:?}", output);
            return Err("Git command failed");
        }
        info!("Git command executed successfully.");
        Ok(())
    }


    fn load_author(repo: &git2::Repository) -> Result<String, &'static str> {
        debug!("Loading author from git config.");
        let cfg = match repo.config() {
            Ok(c)  => c,
            Err(e) => {
                error!("Failed to load git config: {}", e);
                return Err("Failed to load git config");
            }
        };

        let name  = match cfg.get_string("user.name") {
            Ok(s)  => s,
            Err(e) => {
                error!("Failed to find git user name: {}", e);
                return Err("Failed to find git user name");
            }
        };
        debug!("Found git user name: {}", name);

        let email = match cfg.get_string("user.email") {
            Ok(s)  => s,
            Err(e) => {
                error!("Failed to find git email address: {}", e);
                return Err("Failed to find git email address");
            }
        };
        debug!("Found git email address: {}", email);

        Ok(format!("{} <{}>", name, email))
    }

    fn ensure_gnostr_dirs_exist(repo_root_path: &Path) -> Result<(), &'static str> {
        debug!("Ensuring .gnostr directories exist in: {}", repo_root_path.display());
        let gnostr_path = repo_root_path.join(".gnostr");
        let blobs_path = gnostr_path.join("blobs");
        let reflog_path = gnostr_path.join("reflog");

        std::fs::create_dir_all(&gnostr_path).map_err(|_| "Failed to create .gnostr directory")?;
        std::fs::create_dir_all(&blobs_path).map_err(|_| "Failed to create .gnostr/blobs directory")?;
        std::fs::create_dir_all(&reflog_path).map_err(|_| "Failed to create .gnostr/reflog directory")?;

        Ok(())
    }

    fn prepare_tree(repo: &mut git2::Repository) -> Result<(String, String), &'static str> {
        debug!("Preparing tree.");
        Gitminer::ensure_no_unstaged_changes(repo)?;

        let head = match repo.revparse_single("HEAD") {
            Ok(h) => h,
            Err(_) => return Err("Failed to get HEAD"),
        };
        let mut index = match repo.index() {
            Ok(i) => i,
            Err(_) => return Err("Failed to get index"),
        };
        let tree = match index.write_tree() {
            Ok(t) => t,
            Err(_) => return Err("Failed to write tree"),
        };

        let head_s = format!("{}", head.id());
        let tree_s = format!("{}", tree);
        debug!("Head: {}, Tree: {}", head_s, tree_s);

        Ok((tree_s, head_s))
    }

    fn ensure_no_unstaged_changes(repo: &mut git2::Repository) -> Result<(), &'static str> {
        debug!("Ensuring no unstaged changes.");
        let mut opts = git2::StatusOptions::new();
        let mut m    = git2::Status::empty();
        let statuses = match repo.statuses(Some(&mut opts)) {
            Ok(s) => s,
            Err(_) => return Err("Failed to get statuses"),
        };

        m.insert(git2::Status::WT_NEW);
        m.insert(git2::Status::WT_MODIFIED);
        m.insert(git2::Status::WT_DELETED);
        m.insert(git2::Status::WT_RENAMED);
        m.insert(git2::Status::WT_TYPECHANGE);

        for i in 0..statuses.len() {
            let status_entry = match statuses.get(i) {
                Some(s) => s,
                None => return Err("Failed to get status entry"),
            };
            if status_entry.status().intersects(m) {
                return Err("Please stash all unstaged changes before running.");
            }
        }

        debug!("No unstaged changes found.");
        Ok(())
    }

}
