use crate::bucket::Bucket;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::io::Read;
use std::io::Write;

const INDEX_HTML: &str = include_str!("../web/index.html");
const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");

/// Parses a version string like "1.2.3" into (major, minor, patch)
fn parse_version(version: &str) -> Option<(u32, u32, u32)> {
    let parts: Vec<&str> = version.split('.').collect();
    if parts.len() != 3 {
        return None;
    }
    let major = parts[0].parse().ok()?;
    let minor = parts[1].parse().ok()?;
    let patch = parts[2].parse().ok()?;
    Some((major, minor, patch))
}

/// Compares two versions. Returns Ordering.
fn compare_versions(a: (u32, u32, u32), b: (u32, u32, u32)) -> std::cmp::Ordering {
    a.cmp(&b)
}

pub struct BucketRemote {
    #[allow(dead_code)]
    bucket: Bucket,
    force: bool,
    version_checked: bool,
}

async fn upload_object(
    bucket: &mut Bucket,
    repo: &gix::ThreadSafeRepository,
    oid: gix::ObjectId,
    kind: gix::object::Kind,
) -> Result<(), String> {
    let hex_id = oid.to_string();
    let key = format!("objects/{}/{}", &hex_id[..2], &hex_id[2..]);

    if bucket.exists(key.clone()).await {
        return Ok(());
    }

    let data = {
        let local_repo = repo.to_thread_local();
        let obj = local_repo
            .find_object(oid)
            .map_err(|e| format!("Failed to find object {}: {}", oid, e))?;
        obj.data.to_vec()
    };

    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
    let header = format!("{} {}\0", kind, data.len());
    encoder
        .write_all(header.as_bytes())
        .map_err(|e| e.to_string())?;
    encoder.write_all(&data).map_err(|e| e.to_string())?;
    let compressed = encoder.finish().map_err(|e| e.to_string())?;

    bucket.put(key, compressed).await;
    Ok(())
}

async fn upload_tree<'a>(
    bucket: &'a mut Bucket,
    repo: &'a gix::ThreadSafeRepository,
    tree_id: gix::ObjectId,
) -> Result<(), String> {
    let hex_id = tree_id.to_string();
    let key = format!("objects/{}/{}", &hex_id[..2], &hex_id[2..]);
    if bucket.exists(key).await {
        return Ok(());
    }

    let entries: Vec<(gix::ObjectId, gix::object::Kind)> = {
        let local_repo = repo.to_thread_local();
        let tree = local_repo
            .find_object(tree_id)
            .map_err(|e| format!("Failed to find tree {}: {}", tree_id, e))?
            .into_tree();

        let mut collected = Vec::new();
        for e in tree.iter() {
            let e = e.map_err(|e| e.to_string())?;
            let kind = match e.mode().kind() {
                gix::object::tree::EntryKind::Tree => Some(gix::object::Kind::Tree),
                gix::object::tree::EntryKind::Blob
                | gix::object::tree::EntryKind::BlobExecutable
                | gix::object::tree::EntryKind::Link => Some(gix::object::Kind::Blob),
                _ => None,
            };
            if let Some(k) = kind {
                collected.push((e.oid().to_owned(), k));
            }
        }
        collected
    };

    for (oid, kind) in entries {
        match kind {
            gix::object::Kind::Blob => {
                upload_object(bucket, repo, oid, kind).await?;
            }
            gix::object::Kind::Tree => {
                Box::pin(upload_tree(bucket, repo, oid)).await?;
            }
            _ => {}
        }
    }

    // Upload the tree itself
    upload_object(bucket, repo, tree_id, gix::object::Kind::Tree).await?;
    Ok(())
}

async fn fetch_recursive<'a>(
    bucket: &'a mut Bucket,
    repo: &'a gix::ThreadSafeRepository,
    oid: gix::ObjectId,
) -> Result<(), String> {
    let hex = oid.to_string();
    let rel_path = format!("objects/{}/{}", &hex[..2], &hex[2..]);

    // Check if object exists in local repo using gix
    {
        let local_repo = repo.to_thread_local();
        if local_repo.has_object(oid) {
            return Ok(());
        }
    }

    if !bucket.exists(rel_path.clone()).await {
        return Err(format!("Object {} not found on remote", oid));
    }

    let compressed_data = bucket.get(rel_path).await;

    let mut decoder = flate2::read::ZlibDecoder::new(&compressed_data[..]);
    let mut decompressed = Vec::new();
    decoder
        .read_to_end(&mut decompressed)
        .map_err(|e| e.to_string())?;

    let null_pos = decompressed
        .iter()
        .position(|&b| b == 0)
        .ok_or("Invalid object format: no null byte")?;
    let header = &decompressed[..null_pos];
    let body = &decompressed[null_pos + 1..];

    let header_str = String::from_utf8_lossy(header);
    let mut parts = header_str.split_whitespace();
    let kind_str = parts.next().ok_or("Invalid header")?;

    let kind = match kind_str {
        "commit" => gix::object::Kind::Commit,
        "tree" => gix::object::Kind::Tree,
        "blob" => gix::object::Kind::Blob,
        "tag" => gix::object::Kind::Tag,
        _ => return Err(format!("Unknown object kind: {}", kind_str)),
    };

    // Write object to local repo using gix
    {
        let local_repo = repo.to_thread_local();
        use gix::prelude::Write;
        local_repo
            .objects
            .write_buf(kind, body)
            .map_err(|e| format!("Failed to write object: {}", e))?;
    }

    match kind {
        gix::object::Kind::Commit => {
            let body_str = String::from_utf8_lossy(body);
            for line in body_str.lines() {
                if line.starts_with("tree ") {
                    let tree_hex = line.strip_prefix("tree ").unwrap().trim();
                    if let Ok(tree_oid) = gix::ObjectId::from_hex(tree_hex.as_bytes()) {
                        Box::pin(fetch_recursive(bucket, repo, tree_oid)).await?;
                    }
                } else if line.starts_with("parent ") {
                    let parent_hex = line.strip_prefix("parent ").unwrap().trim();
                    if let Ok(parent_oid) = gix::ObjectId::from_hex(parent_hex.as_bytes()) {
                        Box::pin(fetch_recursive(bucket, repo, parent_oid)).await?;
                    }
                } else if line.is_empty() {
                    break;
                }
            }
        }
        gix::object::Kind::Tree => {
            let mut i = 0;
            while i < body.len() {
                let mode_end = body[i..]
                    .iter()
                    .position(|&b| b == b' ')
                    .ok_or("Invalid tree entry")?
                    + i;
                let null_pos = body[mode_end..]
                    .iter()
                    .position(|&b| b == 0)
                    .ok_or("Invalid tree entry")?
                    + mode_end;
                i = null_pos + 1;
                if i + 20 > body.len() {
                    break;
                }
                let hash = &body[i..i + 20];
                let oid = gix::ObjectId::from_bytes_or_panic(hash);

                Box::pin(fetch_recursive(bucket, repo, oid)).await?;

                i += 20;
            }
        }
        gix::object::Kind::Tag => {
            let body_str = String::from_utf8_lossy(body);
            for line in body_str.lines() {
                if line.starts_with("object ") {
                    let obj_hex = line.strip_prefix("object ").unwrap().trim();
                    if let Ok(obj_oid) = gix::ObjectId::from_hex(obj_hex.as_bytes()) {
                        Box::pin(fetch_recursive(bucket, repo, obj_oid)).await?;
                    }
                    break;
                }
            }
        }
        _ => {}
    }

    Ok(())
}

impl BucketRemote {
    pub fn new(bucket: Bucket) -> Self {
        BucketRemote {
            bucket,
            force: false,
            version_checked: false,
        }
    }

    /// Checks if index.html needs to be updated based on version comparison.
    /// Should be called once per session (tracks via version_checked flag).
    /// Returns Err with a message if the remote has a newer major version.
    async fn check_and_update_index_html(&mut self) -> Result<(), String> {
        if self.version_checked {
            return Ok(());
        }
        self.version_checked = true;

        let current_version = parse_version(CRATE_VERSION)
            .ok_or_else(|| format!("Failed to parse crate version: {}", CRATE_VERSION))?;

        let should_update = if self.bucket.exists("index.html".to_string()).await {
            let existing = self.bucket.get("index.html".to_string()).await;
            let existing_str = String::from_utf8_lossy(&existing);

            // Look for !!J3_REPO_VIEWER_VERSION!!:x.y.z
            let version_pattern = "!!J3_REPO_VIEWER_VERSION!!:";
            if let Some(pos) = existing_str.find(version_pattern) {
                let after_marker = &existing_str[pos + version_pattern.len()..];
                // Extract version number (digits and dots until non-matching char)
                let version_end = after_marker
                    .find(|c: char| !c.is_ascii_digit() && c != '.')
                    .unwrap_or(after_marker.len());
                let remote_version_str = &after_marker[..version_end];

                if let Some(remote_version) = parse_version(remote_version_str) {
                    // Check if remote major version is greater than ours
                    if remote_version.0 > current_version.0 {
                        return Err(format!(
                            "Remote index.html has version {} which is incompatible with j3 {}.\n\
                             Please update j3 with: brew install -f j3",
                            remote_version_str, CRATE_VERSION
                        ));
                    }

                    // Update if remote version is less than current
                    compare_versions(remote_version, current_version) == std::cmp::Ordering::Less
                } else {
                    // Couldn't parse remote version, update it
                    true
                }
            } else {
                // Version marker not found, update it
                true
            }
        } else {
            // index.html doesn't exist, create it
            true
        };

        if should_update {
            let html_content = INDEX_HTML.replace("$J3_VERSION$", CRATE_VERSION);
            self.bucket
                .put("index.html".to_string(), html_content.as_bytes().to_vec())
                .await;
        }

        Ok(())
    }
}

/// determines which commits the remote server is missing to have `local`'s ancestors fully synced
/// this returns results in REVERSE topological order — i think maybe it's nice for the remote repository
/// to never contain a commit that references as parent another commit not present, but perhaps in practice these
/// 'orphan' children don't matter since no HEAD will ever point to them? but doesn't hurt.
fn missing_commits_from_server<'a>(
    repo: &'a gix::Repository,
    local: gix::ObjectId,
    remote_heads: Vec<gix::ObjectId>,
) -> impl Iterator<Item = gix::traverse::commit::Info> + 'a {
    let vec: Vec<_> = gix::traverse::commit::topo::Builder::new(repo)
        .with_tips([local])
        .with_ends(remote_heads)
        .build()
        .unwrap()
        .map(|r| r.unwrap())
        .collect();
    vec.into_iter()
}

impl crate::git_remote::GitRemoteHelper for BucketRemote {
    async fn list(&mut self, _for_push: bool) -> Vec<crate::git_remote::Ref> {
        // Check and update index.html version (only happens once per session)
        if let Err(e) = self.check_and_update_index_html().await {
            eprintln!("Error: {}", e);
            std::process::exit(1);
        }

        let mut refs = Vec::new();

        async fn resolve_ref(
            bucket: &mut Bucket,
            name: String,
            bytes: Vec<u8>,
        ) -> Vec<crate::git_remote::Ref> {
            let mut results = Vec::new();
            let content = String::from_utf8_lossy(&bytes).trim().to_string();

            if let Some(target) = content.strip_prefix("ref: ") {
                results.push(crate::git_remote::Ref {
                    points_at: format!("@{}", target),
                    name: name.clone(),
                });
                // Resolve the target
                if bucket.exists(target.to_string()).await {
                    let target_bytes = bucket.get(target.to_string()).await;
                    let target_content = String::from_utf8_lossy(&target_bytes).trim().to_string();
                    if let Ok(oid) = gix::ObjectId::from_hex(target_content.as_bytes()) {
                        results.push(crate::git_remote::Ref {
                            points_at: oid.to_string(),
                            name,
                        });
                    }
                }
            } else if let Ok(oid) = gix::ObjectId::from_hex(content.as_bytes()) {
                results.push(crate::git_remote::Ref {
                    points_at: oid.to_string(),
                    name,
                });
            }
            results
        }

        // Check for HEAD
        if self.bucket.exists("HEAD".to_string()).await {
            let bytes = self.bucket.get("HEAD".to_string()).await;
            refs.extend(resolve_ref(&mut self.bucket, "HEAD".to_string(), bytes).await);
        }

        // List refs/
        let keys = self.bucket.list_files("refs/".to_string()).await;
        for key in keys {
            let bytes = self.bucket.get(key.clone()).await;
            refs.extend(resolve_ref(&mut self.bucket, key, bytes).await);
        }

        refs
    }

    async fn option(
        &mut self,
        name: String,
        value: String,
    ) -> Result<(), crate::git_remote::OptionError> {
        match name.as_str() {
            "force" => match value.as_str() {
                "true" => {
                    self.force = true;
                    Ok(())
                }
                "false" => {
                    self.force = false;
                    Ok(())
                }
                _ => Err(crate::git_remote::OptionError::Error(format!(
                    "Invalid value for force: {}",
                    value
                ))),
            },
            _ => Err(crate::git_remote::OptionError::Unsupported),
        }
    }

    async fn fetch(&mut self, points_at: String, _name: String) -> Result<(), String> {
        let repo = gix::open_with_environment_overrides(".")
            .map_err(|e| format!("Failed to open git repo: {}", e))?;
        let shared_repo = repo.into_sync();
        let oid = gix::ObjectId::from_hex(points_at.as_bytes())
            .map_err(|e| format!("Invalid OID: {}", e))?;

        fetch_recursive(&mut self.bucket, &shared_repo, oid).await
    }

    async fn push(&mut self, src: String, dst: String) -> Result<(), String> {
        // Check and update index.html version first (may abort if incompatible)
        self.check_and_update_index_html().await?;

        let (src, force) = if let Some(s) = src.strip_prefix('+') {
            (s, true)
        } else {
            (src.as_str(), self.force)
        };

        let repo = gix::open_with_environment_overrides(".")
            .map_err(|e| format!("Failed to open git repo: {}", e))?;
        let local_oid = repo
            .rev_parse_single(src)
            .map_err(|e| format!("Failed to parse ref {}: {}", src, e))?
            .detach();

        let remote_refs = self.list(true).await;

        // Check for non-fast-forward
        if !force {
            if let Some(remote_ref) = remote_refs.iter().find(|r| r.name == dst) {
                if let Ok(remote_oid) = gix::ObjectId::from_hex(remote_ref.points_at.as_bytes()) {
                    let local_commit = repo
                        .find_object(local_oid)
                        .map_err(|e| e.to_string())?
                        .into_commit();
                    // check if remote_oid is reachable from local_commit
                    let is_ancestor = local_commit
                        .ancestors()
                        .all()
                        .map_err(|e| e.to_string())?
                        .filter_map(|r| r.ok())
                        .any(|info| info.id == remote_oid);

                    if !is_ancestor {
                        return Err(format!("non-fast-forward"));
                    }
                }
            }
        }

        let remote_heads = remote_refs
            .iter()
            .filter_map(|r| gix::ObjectId::from_hex(r.points_at.as_bytes()).ok())
            .collect();

        let commits: Vec<_> = missing_commits_from_server(&repo, local_oid, remote_heads).collect();
        let shared_repo = repo.into_sync();

        for commit in commits {
            let tree_id = {
                let local_repo = shared_repo.to_thread_local();
                let obj = local_repo
                    .find_object(commit.id)
                    .map_err(|e| e.to_string())?;
                let commit = obj.into_commit();
                let tid = commit.tree_id().map_err(|e| e.to_string())?;
                tid.detach()
            };

            upload_tree(&mut self.bucket, &shared_repo, tree_id).await?;

            upload_object(
                &mut self.bucket,
                &shared_repo,
                commit.id,
                gix::object::Kind::Commit,
            )
            .await?;
        }

        self.bucket
            .put(dst.clone(), local_oid.to_string().as_bytes().to_vec())
            .await;

        // 1. Ensure objects/info/packs exists (empty for now, as we don't support packing yet)
        self.bucket
            .put("objects/info/packs".to_string(), Vec::new())
            .await;

        // 2. Check/Create HEAD
        if !self.bucket.exists("HEAD".to_string()).await {
            let head_content = format!("ref: {}\n", dst);
            self.bucket
                .put("HEAD".to_string(), head_content.as_bytes().to_vec())
                .await;
        }

        // 3. Update info/refs
        let all_refs = self.list(false).await;
        let mut info_refs_content = String::new();
        for r in all_refs {
            // Skip symrefs (which list returns prefixed with @)
            if !r.points_at.starts_with('@') {
                info_refs_content.push_str(&format!("{}\t{}\n", r.points_at, r.name));
            }
        }
        self.bucket
            .put(
                "info/refs".to_string(),
                info_refs_content.as_bytes().to_vec(),
            )
            .await;

        Ok(())
    }
}
