use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::io::{Cursor, Read, Write};
use anyhow::{Context, Result, anyhow};
use git2::{Repository, Oid, ObjectType, Signature, Index, IndexEntry, IndexTime};
use sha2::{Sha256, Digest};

use crate::traits::{ContextStore, Transaction};
use crate::types::{DirEntry, EntryKind, FileChange, BlobRef};

pub struct Vgcs {
    root_path: PathBuf,
}

impl Vgcs {
    pub fn new<P: AsRef<Path>>(path: P) -> Self {
        Self {
            root_path: path.as_ref().to_path_buf(),
        }
    }

    fn get_repo_path(&self, req_id: &str) -> PathBuf {
        self.root_path.join("repos").join(format!("{}.git", req_id))
    }

    fn get_blob_store_root(&self, req_id: &str) -> PathBuf {
        self.root_path.join("blobs").join(req_id)
    }

    fn get_blob_path(&self, req_id: &str, sha256: &str) -> PathBuf {
        self.get_blob_store_root(req_id)
            .join(&sha256[0..2])
            .join(sha256)
    }
}

impl ContextStore for Vgcs {
    fn init_repo(&self, req_id: &str) -> Result<()> {
        let repo_path = self.get_repo_path(req_id);
        if !repo_path.exists() {
            fs::create_dir_all(&repo_path).context("Failed to create repo dir")?;
            Repository::init_bare(&repo_path).context("Failed to init bare repo")?;
        }
        Ok(())
    }

    fn read_file(&self, req_id: &str, commit_hash: &str, path: &str) -> Result<Box<dyn Read + Send>> {
        let repo_path = self.get_repo_path(req_id);
        let repo = Repository::open(&repo_path).context("Failed to open repo")?;
        
        let oid = Oid::from_str(commit_hash).context("Invalid commit hash")?;
        let commit = repo.find_commit(oid).context("Commit not found")?;
        let tree = commit.tree().context("Tree not found")?;
        
        let entry = tree.get_path(Path::new(path)).context("File not found in tree")?;
        let object = entry.to_object(&repo).context("Object not found")?;
        
        if let Some(blob) = object.as_blob() {
            let content = blob.content();
            // Try parsing as BlobRef
            if let Ok(blob_ref) = serde_json::from_slice::<BlobRef>(content) {
                if blob_ref.vgcs_ref == "v1" {
                    let blob_path = self.get_blob_path(req_id, &blob_ref.sha256);
                    let file = File::open(blob_path).context("Failed to open blob file from store")?;
                    return Ok(Box::new(file));
                }
            }
            // Return raw content
            return Ok(Box::new(Cursor::new(content.to_vec())));
        }
        
        Err(anyhow!("Path is not a file"))
    }

    fn list_dir(&self, req_id: &str, commit_hash: &str, path: &str) -> Result<Vec<DirEntry>> {
        let repo_path = self.get_repo_path(req_id);
        let repo = Repository::open(&repo_path).context("Failed to open repo")?;
        
        let oid = Oid::from_str(commit_hash).context("Invalid commit hash")?;
        let commit = repo.find_commit(oid).context("Commit not found")?;
        let root_tree = commit.tree().context("Tree not found")?;
        
        let tree = if path.is_empty() || path == "/" || path == "." {
            root_tree
        } else {
            let entry = root_tree.get_path(Path::new(path)).context("Path not found")?;
            let object = entry.to_object(&repo).context("Object not found")?;
            object.into_tree().map_err(|_| anyhow!("Path is not a directory"))?
        };

        let mut entries = Vec::new();
        for entry in tree.iter() {
            let name = entry.name().unwrap_or("").to_string();
            let kind = match entry.kind() {
                 Some(ObjectType::Tree) => EntryKind::Dir,
                 _ => EntryKind::File,
            };
            let object_id = entry.id().to_string();

            // Metadata extraction (Expensive but necessary for the prompt)
            let mut size = None;
            let mut line_count = None;
            let mut word_count = None;

            if kind == EntryKind::File {
                 if let Ok(object) = entry.to_object(&repo) {
                     if let Some(blob) = object.as_blob() {
                         let content = blob.content();
                         size = Some(content.len() as u64);
                         
                         // Check for binary content or just use heuristic
                         if !content.contains(&0) {
                             let s = String::from_utf8_lossy(content);
                             line_count = Some(s.lines().count());
                             word_count = Some(s.split_whitespace().count());
                         }
                     }
                 }
            }

            entries.push(DirEntry { name, kind, object_id, size, line_count, word_count });
        }
        
        Ok(entries)
    }

    fn diff(&self, req_id: &str, from_commit: &str, to_commit: &str) -> Result<Vec<FileChange>> {
        let repo_path = self.get_repo_path(req_id);
        let repo = Repository::open(&repo_path).context("Failed to open repo")?;
        
        let from_oid = Oid::from_str(from_commit).context("Invalid from_commit")?;
        let to_oid = Oid::from_str(to_commit).context("Invalid to_commit")?;
        
        let from_tree = repo.find_commit(from_oid)?.tree()?;
        let to_tree = repo.find_commit(to_oid)?.tree()?;
        
        let diff = repo.diff_tree_to_tree(Some(&from_tree), Some(&to_tree), None)?;
        
        let mut changes = Vec::new();
        diff.foreach(&mut |delta, _| {
            let path = delta.new_file().path().or(delta.old_file().path()).unwrap();
            let path_str = path.to_string_lossy().to_string();
            
            match delta.status() {
                git2::Delta::Added => changes.push(FileChange::Added(path_str)),
                git2::Delta::Deleted => changes.push(FileChange::Deleted(path_str)),
                git2::Delta::Modified => changes.push(FileChange::Modified(path_str)),
                _ => {}
            }
            true
        }, None, None, None)?;
        
        Ok(changes)
    }

    fn merge_trees(&self, req_id: &str, base: &str, ours: &str, theirs: &str) -> Result<String> {
        let repo_path = self.get_repo_path(req_id);
        let repo = Repository::open(&repo_path).context("Failed to open repo")?;
        
        let base_tree = repo.find_commit(Oid::from_str(base)?)?.tree()?;
        let our_tree = repo.find_commit(Oid::from_str(ours)?)?.tree()?;
        let their_tree = repo.find_commit(Oid::from_str(theirs)?)?.tree()?;
        
        let mut index = repo.merge_trees(&base_tree, &our_tree, &their_tree, None)?;
        
        if index.has_conflicts() {
            return Err(anyhow!("Merge conflict detected"));
        }
        
        let oid = index.write_tree_to(&repo)?;
        Ok(oid.to_string())
    }

    fn merge_commits(&self, req_id: &str, our_commit: &str, their_commit: &str) -> Result<String> {
        let repo_path = self.get_repo_path(req_id);
        let repo = Repository::open(&repo_path).context("Failed to open repo")?;

        let our_oid = Oid::from_str(our_commit).context("Invalid our_commit")?;
        let their_oid = Oid::from_str(their_commit).context("Invalid their_commit")?;

        let base_oid = repo.merge_base(our_oid, their_oid).context("Failed to find merge base")?;
        
        let base_commit = repo.find_commit(base_oid)?;
        let our_commit_obj = repo.find_commit(our_oid)?;
        let their_commit_obj = repo.find_commit(their_oid)?;

        // If base equals one of the commits, it's a fast-forward
        if base_oid == our_oid {
            return Ok(their_commit.to_string());
        }
        if base_oid == their_oid {
            return Ok(our_commit.to_string());
        }

        let base_tree = base_commit.tree()?;
        let our_tree = our_commit_obj.tree()?;
        let their_tree = their_commit_obj.tree()?;

        let mut index = repo.merge_trees(&base_tree, &our_tree, &their_tree, None)?;

        if index.has_conflicts() {
             return Err(anyhow!("Merge conflict detected between {} and {}", our_commit, their_commit));
        }

        let tree_oid = index.write_tree_to(&repo)?;
        let tree = repo.find_tree(tree_oid)?;

        let sig = Signature::now("vgcs-merge", "system")?;

        let merge_commit_oid = repo.commit(
            None, // Detached
            &sig,
            &sig,
            &format!("Merge commit {} into {}", their_commit, our_commit),
            &tree,
            &[&our_commit_obj, &their_commit_obj],
        )?;

        Ok(merge_commit_oid.to_string())
    }

    fn begin_transaction(&self, req_id: &str, base_commit: &str) -> Result<Box<dyn Transaction>> {
        let repo_path = self.get_repo_path(req_id);
        let repo = Repository::open(&repo_path).context("Failed to open repo")?;
        
        let mut index = Index::new()?;
        let mut base_commit_oid = None;

        if !base_commit.is_empty() {
            let base_oid = Oid::from_str(base_commit).context("Invalid base_commit")?;
            if !base_oid.is_zero() {
                // Scope the borrow of repo
                {
                    let commit = repo.find_commit(base_oid).context("Base commit not found")?;
                    let tree = commit.tree()?;
                    index.read_tree(&tree)?;
                }
                base_commit_oid = Some(base_oid);
            }
        }
        
        Ok(Box::new(VgcsTransaction {
            repo,
            req_id: req_id.to_string(),
            root_path: self.root_path.clone(),
            base_commit: base_commit_oid,
            index,
        }))
    }
}

pub struct VgcsTransaction {
    repo: Repository,
    req_id: String,
    root_path: PathBuf,
    base_commit: Option<Oid>,
    index: Index,
}

impl Transaction for VgcsTransaction {
    fn write(&mut self, path: &str, content: &[u8]) -> Result<()> {
        let final_content = if content.len() > 1024 * 1024 { // 1MB
            // Calculate SHA256
            let mut hasher = Sha256::new();
            hasher.update(content);
            let result = hasher.finalize();
            let sha256 = hex::encode(result);
            
            // Write to Blob Store
            let blob_path = self.root_path
                .join("blobs")
                .join(&self.req_id)
                .join(&sha256[0..2])
                .join(&sha256);
                
            if !blob_path.exists() {
                if let Some(parent) = blob_path.parent() {
                    fs::create_dir_all(parent)?;
                }
                let mut file = File::create(&blob_path)?;
                file.write_all(content)?;
            }
            
            // Create BlobRef JSON
            let blob_ref = BlobRef {
                vgcs_ref: "v1".to_string(),
                sha256: sha256,
                size: content.len() as u64,
                mime_type: "application/octet-stream".to_string(), // Simplified
                original_name: Path::new(path).file_name().unwrap_or_default().to_string_lossy().to_string(),
            };
            
            serde_json::to_vec(&blob_ref)?
        } else {
            content.to_vec()
        };

        // Write to ODB manually
        let oid = self.repo.blob(&final_content)?;
        
        let mut entry = create_index_entry(path, 0o100644);
        entry.id = oid;
        entry.file_size = final_content.len() as u32;
        
        self.index.add(&entry).context("Failed to add entry to index")?;
        
        Ok(())
    }

    fn remove(&mut self, path: &str) -> Result<()> {
        self.index.remove_path(Path::new(path))?;
        Ok(())
    }

    fn commit(mut self: Box<Self>, message: &str, author: &str) -> Result<String> {
        let tree_oid = self.index.write_tree_to(&self.repo)?;
        let tree = self.repo.find_tree(tree_oid)?;
        
        let sig = Signature::now(author, "vgcs@system")?;
        
        let commit_oid = if let Some(base_oid) = self.base_commit {
            let parent_commit = self.repo.find_commit(base_oid)?;
            self.repo.commit(
                None, // Detached commit
                &sig,
                &sig,
                message,
                &tree,
                &[&parent_commit],
            )?
        } else {
            self.repo.commit(
                None, // Detached commit
                &sig,
                &sig,
                message,
                &tree,
                &[],
            )?
        };
        
        Ok(commit_oid.to_string())
    }
}

fn create_index_entry(path: &str, mode: u32) -> IndexEntry {
    IndexEntry {
        ctime: IndexTime::new(0, 0),
        mtime: IndexTime::new(0, 0),
        dev: 0,
        ino: 0,
        mode,
        uid: 0,
        gid: 0,
        file_size: 0,
        id: Oid::zero(),
        flags: 0,
        flags_extended: 0,
        path: path.as_bytes().to_vec(),
    }
}
