use crate::config::*;
use crate::repo::*;
use chrono::{DateTime, Utc};
use git2::Repository;
use gleisbau::{
    graph::Builder as GraphBuilder,
    print::{format::CommitFormat, unicode::print_unicode},
    settings::{
        BranchOrder, BranchSettings, BranchSettingsDef, Characters, MergePatterns, Settings,
    },
};
use rayon::prelude::*;
use std::ffi::OsStr;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::Path;
use std::{cmp::Reverse, fs};
use syntect::highlighting::Color;
use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;
use tokio::time::Duration;

pub struct AppState {
    pub simple_repos: Vec<SimpleRepo>,
    pub repos: Vec<Repo>,
    pub files: Vec<FileInfo>,
    pub last_load: DateTime<Utc>,
    pub config: Config,
}
impl AppState {
    pub fn get_simple_repos(&mut self) -> Vec<SimpleRepo> {
        let reload_time = self.last_load + Duration::from_secs(self.config.load_duration);
        if Utc::now() > reload_time {
            self.last_load = Utc::now();
            self.simple_repos = load_simple_repos(self.config.clone());
        }
        self.simple_repos.clone()
    }

    pub fn get_repo(&mut self, name: String) -> Option<&Repo> {
        if let Some(simple_repo) = self.simple_repos.iter().find(|x| x.name == name) {
            let existing = self
                .repos
                .iter()
                .find(|x| x.name == simple_repo.name)
                .map(|r| r.name.clone());
            let mut reload = false;

            if let Some(repo_name) = existing {
                if let Some(repo) = self.repos.iter().find(|x| x.name == repo_name) {
                    let reload_time =
                        repo.last_load + Duration::from_secs(self.config.load_duration);
                    if Utc::now() > reload_time {
                        reload = true;
                    }
                }
            } else {
                reload = true;
            }

            if reload {
                if let Some(reloaded) = load_repo(name.clone(), self.config.clone()) {
                    if let Some(pos) = self.repos.iter().position(|x| x.name == name) {
                        self.repos.swap_remove(pos);
                    }
                    self.repos.push(reloaded);
                    return self.repos.iter().find(|x| x.name == name);
                } else {
                    return None;
                }
            } else {
                return self.repos.iter().find(|x| x.name == simple_repo.name);
            }
        }
        None
    }

    pub fn get_file(&mut self, file_id: String, repo_name: String) -> Option<&FileInfo> {
        let reload;
        if let Some(file) = self.files.iter().find(|x| x.id == file_id) {
            let file_reload_time = file.last_load + Duration::from_secs(self.config.load_duration);
            if Utc::now() > file_reload_time {
                reload = true;
            } else {
                reload = false;
            }
        } else {
            reload = true;
        }
        if reload {
            if let Some(reloaded) = load_file(file_id.clone(), repo_name, self.config.clone()) {
                if let Some(pos) = self.files.iter().position(|x| x.id == file_id) {
                    self.files.swap_remove(pos);
                }
                self.files.push(reloaded);
                return self.files.iter().find(|x| x.id == file_id);
            } else {
                return None;
            }
        } else {
            return self.files.iter().find(|x| x.id == file_id);
        }
    }
}

pub fn load_simple_repos(config: Config) -> Vec<SimpleRepo> {
    tracing::debug!("Reloading simple repos...");

    let git_dir = config.git_dir.clone();
    let private_repos = config.private_repos.clone();

    let read = match fs::read_dir(&git_dir) {
        Ok(r) => r,
        Err(_) => return Vec::new(),
    };

    let entries: Vec<_> = read
        .filter_map(Result::ok)
        .filter(|entry| entry.path().is_dir())
        .filter_map(|entry| {
            let path = entry.path();
            let name = path
                .file_name()
                .and_then(|n| n.to_str())
                .map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
                .unwrap_or_default();

            if private_repos.contains(&name) {
                return None;
            }

            Repository::open(&path).ok()
        })
        .collect();

    let mut repos: Vec<SimpleRepo> = entries
        .into_par_iter()
        .map(|repo| {
            let name;

            // check if repo is repo_name/.git/, happens when the path is the code and not the source
            if repo
                .path()
                .file_name()
                .unwrap_or_default()
                .to_str()
                .unwrap_or_default()
                == ".git"
            {
                name = repo
                    .path()
                    .parent()
                    .and_then(|p| p.file_name())
                    .and_then(|n| n.to_str())
                    .map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
                    .unwrap_or_default();
            } else {
                name = repo
                    .path()
                    .file_name()
                    .and_then(|n| n.to_str())
                    .map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
                    .unwrap_or_default();
            }

            let lastcommittime = repo
                .head()
                .and_then(|h| h.peel_to_commit())
                .map(|c| {
                    let timestamp = c.time().seconds();
                    DateTime::from_timestamp(timestamp, 0).unwrap_or_default()
                })
                .unwrap_or_else(|_| DateTime::default());

            let mut desc = String::new();

            let mut files = Vec::new();
            if let Ok(commit) = repo.head().and_then(|h| h.peel_to_commit()) {
                if let Ok(tree) = commit.tree() {
                    let _ = tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
                        if entry.kind() == Some(git2::ObjectType::Blob) {
                            let full_path = if root.is_empty() {
                                entry.name().unwrap_or("").to_string()
                            } else {
                                format!("{}{}", root, entry.name().unwrap_or(""))
                            };
                            if let Ok(obj) = entry.to_object(&repo) {
                                if let Some(blob) = obj.as_blob() {
                                    let mut s = DefaultHasher::new();
                                    full_path.hash(&mut s);
                                    files.push(File {
                                        path: full_path,
                                        size: blob.size(),
                                        id: format!("{}", s.finish()),
                                    });

                                    match entry.name().unwrap() {
                                        "DESCRIPTION" => {
                                            desc = str::from_utf8(blob.content())
                                                .unwrap_or_default()
                                                .to_string();
                                        }
                                        _ => {}
                                    }
                                }
                            }
                        }
                        git2::TreeWalkResult::Ok
                    });
                }
            }

            SimpleRepo {
                name,
                desc,
                lastcommit: lastcommittime,
            }
        })
        .collect();

    repos.sort_unstable_by_key(|x| Reverse(x.lastcommit));
    tracing::debug!("Finished loading repos");
    repos
}
pub fn load_repo(name: String, config: Config) -> Option<Repo> {
    tracing::debug!("Reloading repo {}...", name);

    let git_dir = config.git_dir.clone();
    let read = match fs::read_dir(&git_dir) {
        Ok(r) => r,
        Err(e) => {
            tracing::error!("Failed to read git dir {}: {}", git_dir, e);
            return None;
        }
    };

    for entry in read.filter_map(Result::ok) {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let repo = match Repository::open(&path) {
            Ok(r) => r,
            Err(e) => {
                tracing::error!("Failed to open repo at {}: {}", path.display(), e);
                continue;
            }
        };

        let repo_name = path
            .file_name()
            .and_then(|n| n.to_str())
            .map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
            .unwrap_or_default();

        if config.private_repos.contains(&repo_name) {
            continue;
        }
        if repo_name != name {
            continue;
        }
        tracing::debug!("Found matching repo: {}", repo_name);

        let branches = repo.branches(Some(git2::BranchType::Local)).unwrap();
        let mut branches_out: Vec<Branch> = branches
            .filter_map(|res| match res {
                Ok((branch, _)) => Some(branch),
                Err(_) => None,
            })
            .filter_map(|branch| {
                let name = branch.name().ok().flatten().map(|s| s.to_string())?;
                let reference = branch.get();
                let oid = reference.target()?;
                let commit = repo.find_commit(oid).ok()?;
                let author = commit.author();
                Some(Branch {
                    name: name,
                    lastcommit: DateTime::from_timestamp(commit.time().seconds(), 0)
                        .unwrap_or_default(),
                    author: match author.name() {
                        Some(n) => n.to_string(),
                        None => String::new(),
                    },
                })
            })
            .collect();
        branches_out.sort_unstable_by_key(|x| Reverse(x.lastcommit));

        let mut readme_text = String::new();
        let mut desc = String::new();

        let mut files = Vec::new();
        if let Ok(commit) = repo.head().and_then(|h| h.peel_to_commit()) {
            if let Ok(tree) = commit.tree() {
                let _ = tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
                    if entry.kind() == Some(git2::ObjectType::Blob) {
                        let full_path = if root.is_empty() {
                            entry.name().unwrap_or("").to_string()
                        } else {
                            format!("{}{}", root, entry.name().unwrap_or(""))
                        };
                        if let Ok(obj) = entry.to_object(&repo) {
                            if let Some(blob) = obj.as_blob() {
                                let mut s = DefaultHasher::new();
                                full_path.hash(&mut s);
                                files.push(File {
                                    path: full_path,
                                    size: blob.size(),
                                    id: format!("{}", s.finish()),
                                });

                                match entry.name().unwrap() {
                                    "README.md" => {
                                        readme_text = str::from_utf8(blob.content())
                                            .unwrap_or_default()
                                            .to_string();
                                    }
                                    "DESCRIPTION" => {
                                        desc = str::from_utf8(blob.content())
                                            .unwrap_or_default()
                                            .to_string();
                                    }
                                    _ => {}
                                }
                            }
                        }
                    }
                    git2::TreeWalkResult::Ok
                });
            }
        }

        let mut readme_html = String::new();
        let parser = pulldown_cmark::Parser::new(&readme_text);
        pulldown_cmark::html::push_html(&mut readme_html, parser);

        let mut git_graph = String::new();
        let wrapping = Some((None, Some(0), Some(8)));
        let style = Characters::round();
        let settings = Settings {
            reverse_commit_order: false,
            debug: false,
            colored: true,
            compact: false,
            include_remote: false,
            format: CommitFormat::Short,
            wrapping,
            characters: style,
            branch_order: BranchOrder::ShortestFirst(true),
            branches: BranchSettings::from(BranchSettingsDef::git_flow())
                .map_err(|err| err.to_string())
                .unwrap(),
            merge_patterns: MergePatterns::default(),
        };
        let graph_builder = GraphBuilder::new()
            .with_settings(&settings)
            .with_repository(repo)
            .with_max_count(config.git_graph_count);
        if let Ok(graph) = graph_builder.build() {
            if let Ok((g_lines, t_lines, _indices)) = print_unicode(&graph, &settings) {
                for (g_line, t_line) in g_lines.iter().zip(t_lines.iter()) {
                    git_graph += &format!(
                        " {}  {}<br>",
                        ansi_to_html::convert(&g_line).unwrap_or_default(),
                        ansi_to_html::convert(&t_line).unwrap_or_default()
                    );
                }
            };
        };

        tracing::debug!("Finished loading repo");

        let found_repo = Repo {
            name: repo_name,
            desc: desc,
            readme: readme_html,
            branches: branches_out,
            commit_graph: git_graph,
            files: files,
            last_load: Utc::now(),
        };
        return Some(found_repo);
    }
    None
}

pub fn load_file(file_id: String, repo: String, config: Config) -> Option<FileInfo> {
    tracing::debug!("Reloading file {}...", file_id);
    let git_dir = config.git_dir.clone();
    let read = match fs::read_dir(&git_dir) {
        Ok(r) => r,
        Err(e) => {
            tracing::error!("Failed to read git dir {}: {}", git_dir, e);
            return None;
        }
    };

    for entry in read.filter_map(Result::ok) {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let repo_path = match Repository::open(&path) {
            Ok(r) => r,
            Err(e) => {
                tracing::error!("Failed to open repo at {}: {}", path.display(), e);
                continue;
            }
        };

        let repo_name = path
            .file_name()
            .and_then(|n| n.to_str())
            .map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
            .unwrap_or_default();

        if repo_name != repo {
            continue;
        }

        if let Ok(commit) = repo_path.head().and_then(|h| h.peel_to_commit()) {
            if let Ok(tree) = commit.tree() {
                let mut result = None;
                let mut ext = String::new();
                let _ = tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
                    if entry.kind() == Some(git2::ObjectType::Blob) {
                        let full_path = if root.is_empty() {
                            entry.name().unwrap_or("").to_string()
                        } else {
                            format!("{}{}", root, entry.name().unwrap_or(""))
                        };

                        if let Ok(obj) = entry.to_object(&repo_path) {
                            if let Some(blob) = obj.as_blob() {
                                let mut s = DefaultHasher::new();
                                full_path.hash(&mut s);
                                let file_id_str = format!("{}", s.finish());

                                if file_id_str == file_id {
                                    result =
                                        Some(String::from_utf8_lossy(blob.content()).to_string());

                                    ext = Path::new(&full_path)
                                        .extension()
                                        .and_then(OsStr::to_str)
                                        .unwrap_or("txt")
                                        .to_string();
                                }
                            }
                        }
                    }
                    git2::TreeWalkResult::Ok
                });

                if let Some(content) = result {
                    let ss = SyntaxSet::load_defaults_newlines();
                    let sr = ss
                        .find_syntax_by_extension(&ext)
                        .unwrap_or(ss.find_syntax_plain_text());
                    let ts = ThemeSet::load_defaults();
                    let mut theme = ts.themes["base16-eighties.dark"].clone();
                    theme.settings.background = Some(Color {
                        r: (0),
                        g: (0),
                        b: (0),
                        a: (0),
                    });
                    let html = highlighted_html_for_string(&content, &ss, &sr, &theme)
                        .unwrap_or(content.clone());

                    tracing::debug!("Finished loading file");
                    return Some(FileInfo {
                        id: file_id,
                        html_content: html,
                        last_load: Utc::now(),
                    });
                }
            }
        }
    }

    None
}