use std::path::{Path, PathBuf};

use skillfile_core::error::SkillfileError;
use skillfile_core::lock::{lock_key, read_lock};
use skillfile_core::models::{short_sha, Entry, Manifest, SourceFields};
use skillfile_core::parser::MANIFEST_NAME;
use skillfile_core::patch::walkdir;
use skillfile_core::patch::{has_dir_patch, has_patch, patch_path, patches_root};
use skillfile_deploy::adapter::{adapters, AdapterScope, DirInstallMode};
use skillfile_deploy::paths::{installed_paths, source_path};
use skillfile_sources::strategy::{content_file, is_dir_entry};
use skillfile_sources::sync::vendor_dir_for;

use super::status::is_modified_local;

#[derive(Debug, PartialEq, Eq)]
struct InstalledLocation {
    path: PathBuf,
    present: bool,
}

impl InstalledLocation {
    fn new(path: PathBuf, present: bool) -> Self {
        Self { path, present }
    }

    fn display_value(&self) -> String {
        if self.present {
            self.path.display().to_string()
        } else {
            format!("{} (not installed)", self.path.display())
        }
    }
}

fn format_source(entry: &Entry) -> Vec<(&'static str, String)> {
    match &entry.source {
        SourceFields::Github {
            owner_repo,
            path_in_repo,
            ref_,
        } => vec![
            ("Source", format!("github ({owner_repo})")),
            ("Path", path_in_repo.clone()),
            ("Ref", ref_.clone()),
        ],
        SourceFields::Gitlab {
            owner_repo,
            path_in_repo,
            ref_,
        } => vec![
            ("Source", format!("gitlab ({owner_repo})")),
            ("Path", path_in_repo.clone()),
            ("Ref", ref_.clone()),
        ],
        SourceFields::Local { path } => vec![("Source", format!("local ({path})"))],
        SourceFields::Url { url } => vec![("Source", format!("url ({url})"))],
    }
}

fn format_lock_status(entry: &Entry, repo_root: &Path) -> String {
    let locked = match read_lock(repo_root) {
        Ok(l) => l,
        Err(e) => return format!("unknown ({e})"),
    };
    let key = lock_key(entry);
    match locked.get(&key) {
        Some(lock_entry) => {
            let sha = short_sha(&lock_entry.sha);
            format!("sha {sha}")
        }
        None => "no".to_string(),
    }
}

fn format_pinned_status(entry: &Entry, repo_root: &Path) -> String {
    if has_patch(entry, repo_root) {
        format!("yes ({})", patch_path(entry, repo_root).display())
    } else if has_dir_patch(entry, repo_root) {
        let dir = patches_root(repo_root)
            .join(entry.entity_type.dir_name())
            .join(&entry.name);
        format!("yes ({})", dir.display())
    } else {
        "no".to_string()
    }
}

fn format_installed_paths(
    entry: &Entry,
    manifest: &Manifest,
    repo_root: &Path,
) -> Result<Vec<String>, SkillfileError> {
    let locations = if is_dir_entry(entry) {
        installed_locations_for_dir_entry(entry, manifest, repo_root)?
    } else {
        installed_locations_for_single_file(entry, manifest, repo_root)?
    };
    if locations.is_empty() {
        Ok(vec!["(not installed)".to_string()])
    } else {
        Ok(locations
            .iter()
            .map(InstalledLocation::display_value)
            .collect())
    }
}

fn installed_locations_for_single_file(
    entry: &Entry,
    manifest: &Manifest,
    repo_root: &Path,
) -> Result<Vec<InstalledLocation>, SkillfileError> {
    let paths = installed_paths(entry, manifest, repo_root)?;
    Ok(paths
        .into_iter()
        .map(|path| InstalledLocation::new(path.clone(), path.exists()))
        .collect())
}

fn installed_locations_for_dir_entry(
    entry: &Entry,
    manifest: &Manifest,
    repo_root: &Path,
) -> Result<Vec<InstalledLocation>, SkillfileError> {
    let mut locations = Vec::new();
    for target in &manifest.install_targets {
        let adapter = adapters().get(&target.adapter).ok_or_else(|| {
            SkillfileError::Manifest(format!("unknown adapter '{}'", target.adapter))
        })?;
        if !adapter.supports(entry.entity_type) {
            continue;
        }
        let ctx = AdapterScope {
            scope: target.scope,
            repo_root,
        };
        let dir_mode = adapter
            .dir_mode(entry.entity_type)
            .unwrap_or(DirInstallMode::Nested);
        if dir_mode == DirInstallMode::Nested {
            let target_dir = adapter.target_dir(entry.entity_type, &ctx);
            locations.push(nested_dir_location(entry, &target_dir));
        } else {
            locations.extend(flat_dir_locations(
                entry,
                repo_root,
                adapter.target_dir(entry.entity_type, &ctx),
            ));
        }
    }
    Ok(locations)
}

fn nested_dir_location(entry: &Entry, target_dir: &Path) -> InstalledLocation {
    let path = target_dir.join(&entry.name);
    InstalledLocation::new(path.clone(), path.is_dir())
}

fn flat_dir_locations(
    entry: &Entry,
    repo_root: &Path,
    target_dir: PathBuf,
) -> Vec<InstalledLocation> {
    let Some(source_dir) = source_path(entry, repo_root).filter(|path| path.is_dir()) else {
        return vec![InstalledLocation::new(target_dir, false)];
    };
    let mut locations: Vec<InstalledLocation> = walkdir(&source_dir)
        .into_iter()
        .filter(|path| path.extension().is_some_and(|ext| ext == "md"))
        .filter_map(|path| {
            path.file_name()
                .map(|name| target_dir.join(name))
                .map(|path| InstalledLocation::new(path.clone(), path.exists()))
        })
        .collect();
    locations.sort_by(|a, b| a.path.cmp(&b.path));
    locations.dedup_by(|a, b| a.path == b.path);
    if locations.is_empty() {
        vec![InstalledLocation::new(target_dir, false)]
    } else {
        locations
    }
}

fn format_cache_path(entry: &Entry, repo_root: &Path) -> String {
    let vdir = vendor_dir_for(entry, repo_root);
    if is_dir_entry(entry) {
        if vdir.is_dir() {
            vdir.display().to_string()
        } else {
            format!("{} (not cached)", vdir.display())
        }
    } else {
        let cf = content_file(entry);
        if cf.is_empty() {
            return "(no cache file)".to_string();
        }
        let cache_file = vdir.join(&cf);
        if cache_file.exists() {
            cache_file.display().to_string()
        } else {
            format!("{} (not cached)", cache_file.display())
        }
    }
}

pub fn cmd_info(name: &str, repo_root: &Path) -> Result<(), SkillfileError> {
    let manifest_path = repo_root.join(MANIFEST_NAME);
    if !manifest_path.exists() {
        return Err(SkillfileError::Manifest(format!(
            "{MANIFEST_NAME} not found in {}. Create one and run `skillfile init`.",
            repo_root.display()
        )));
    }

    let manifest = crate::config::parse_and_resolve(&manifest_path)?;

    let entry = manifest
        .entries
        .iter()
        .find(|e| e.name == name)
        .ok_or_else(|| {
            SkillfileError::Manifest(format!("entry '{name}' not found in Skillfile"))
        })?;

    let label_w = 12;

    println!("{:>label_w$}  {}", "Name:", entry.name);
    println!("{:>label_w$}  {}", "Type:", entry.entity_type);

    for (label, value) in format_source(entry) {
        println!("{:>label_w$}  {value}", format!("{label}:"));
    }

    println!(
        "{:>label_w$}  {}",
        "Locked:",
        format_lock_status(entry, repo_root)
    );
    println!(
        "{:>label_w$}  {}",
        "Pinned:",
        format_pinned_status(entry, repo_root)
    );
    println!(
        "{:>label_w$}  {}",
        "Modified:",
        if is_modified_local(entry, &manifest, repo_root) {
            "yes"
        } else {
            "no"
        }
    );

    let installed = format_installed_paths(entry, &manifest, repo_root)?;
    for (i, path) in installed.iter().enumerate() {
        let label = if i == 0 { "Installed:" } else { "" };
        println!("{label:>label_w$}  {path}");
    }

    println!(
        "{:>label_w$}  {}",
        "Cache:",
        format_cache_path(entry, repo_root)
    );

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use skillfile_core::models::{EntityType, InstallTarget, Scope, SourceFields};

    fn normalize_separators(path: &str) -> String {
        path.replace('\\', "/")
    }

    fn has_normalized_suffix(path: &str, suffix: &str) -> bool {
        normalize_separators(path).ends_with(suffix)
    }

    fn write_manifest(dir: &Path, content: &str) {
        std::fs::write(dir.join(MANIFEST_NAME), content).unwrap();
    }

    fn write_lock(dir: &Path, data: &serde_json::Value) {
        std::fs::write(
            dir.join("Skillfile.lock"),
            serde_json::to_string_pretty(data).unwrap(),
        )
        .unwrap();
    }

    #[test]
    fn info_entry_not_found() {
        let dir = tempfile::tempdir().unwrap();
        write_manifest(dir.path(), "local  skill  foo  skills/foo.md\n");
        let result = cmd_info("nonexistent", dir.path());
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not found"));
    }

    #[test]
    fn info_no_manifest() {
        let dir = tempfile::tempdir().unwrap();
        let result = cmd_info("foo", dir.path());
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not found"));
    }

    #[test]
    fn info_local_entry() {
        let dir = tempfile::tempdir().unwrap();
        let source = dir.path().join("skills/foo.md");
        std::fs::create_dir_all(source.parent().unwrap()).unwrap();
        std::fs::write(&source, "# Foo").unwrap();
        write_manifest(dir.path(), "local  skill  foo  skills/foo.md\n");
        // Should succeed without error
        cmd_info("foo", dir.path()).unwrap();
    }

    #[test]
    fn info_github_entry_locked() {
        let dir = tempfile::tempdir().unwrap();
        let sha = "87321636a1c666283d8f17398b45c2644395044b";
        write_manifest(
            dir.path(),
            "github  agent  my-agent  owner/repo  agents/agent.md  main\n",
        );
        write_lock(
            dir.path(),
            &serde_json::json!({"github/agent/my-agent": {"sha": sha, "raw_url": "https://example.com"}}),
        );
        cmd_info("my-agent", dir.path()).unwrap();
    }

    #[test]
    fn info_github_entry_unlocked() {
        let dir = tempfile::tempdir().unwrap();
        write_manifest(
            dir.path(),
            "github  agent  my-agent  owner/repo  agents/agent.md  main\n",
        );
        cmd_info("my-agent", dir.path()).unwrap();
    }

    #[test]
    fn format_source_github() {
        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "browser".into(),
            source: SourceFields::Github {
                owner_repo: "anthropics/skills".into(),
                path_in_repo: "skills/browser.md".into(),
                ref_: "main".into(),
            },
        };
        let fields = format_source(&entry);
        assert_eq!(fields.len(), 3);
        assert!(fields[0].1.contains("anthropics/skills"));
        assert_eq!(fields[1].1, "skills/browser.md");
        assert_eq!(fields[2].1, "main");
    }

    #[test]
    fn format_source_local() {
        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "foo".into(),
            source: SourceFields::Local {
                path: "skills/foo.md".into(),
            },
        };
        let fields = format_source(&entry);
        assert_eq!(fields.len(), 1);
        assert!(fields[0].1.contains("skills/foo.md"));
    }

    #[test]
    fn format_source_url() {
        let entry = Entry {
            entity_type: EntityType::Agent,
            name: "my-agent".into(),
            source: SourceFields::Url {
                url: "https://example.com/agent.md".into(),
            },
        };
        let fields = format_source(&entry);
        assert_eq!(fields.len(), 1);
        assert!(fields[0].1.contains("https://example.com/agent.md"));
    }

    #[test]
    fn lock_status_locked() {
        let dir = tempfile::tempdir().unwrap();
        let sha = "abcdef1234567890abcdef1234567890abcdef12";
        let entry = Entry {
            entity_type: EntityType::Agent,
            name: "my-agent".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "agents/agent.md".into(),
                ref_: "main".into(),
            },
        };
        write_lock(
            dir.path(),
            &serde_json::json!({"github/agent/my-agent": {"sha": sha, "raw_url": "https://example.com"}}),
        );
        let status = format_lock_status(&entry, dir.path());
        assert!(status.contains("abcdef123456"));
    }

    #[test]
    fn lock_status_unlocked() {
        let dir = tempfile::tempdir().unwrap();
        let entry = Entry {
            entity_type: EntityType::Agent,
            name: "my-agent".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "agents/agent.md".into(),
                ref_: "main".into(),
            },
        };
        let status = format_lock_status(&entry, dir.path());
        assert_eq!(status, "no");
    }

    #[test]
    fn pinned_status_not_pinned() {
        let dir = tempfile::tempdir().unwrap();
        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "foo".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "skills/foo.md".into(),
                ref_: "main".into(),
            },
        };
        assert_eq!(format_pinned_status(&entry, dir.path()), "no");
    }

    #[test]
    fn pinned_status_single_file_pinned() {
        let dir = tempfile::tempdir().unwrap();
        let patches_dir = dir.path().join(".skillfile/patches/skills");
        std::fs::create_dir_all(&patches_dir).unwrap();
        std::fs::write(patches_dir.join("foo.patch"), "patch").unwrap();

        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "foo".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "skills/foo.md".into(),
                ref_: "main".into(),
            },
        };
        let status = format_pinned_status(&entry, dir.path());
        assert!(
            status.starts_with("yes"),
            "expected 'yes ...', got: {status}"
        );
        assert!(
            has_normalized_suffix(&status, ".skillfile/patches/skills/foo.patch)"),
            "expected patch path, got: {status}"
        );
    }

    #[test]
    fn pinned_status_dir_entry_pinned() {
        let dir = tempfile::tempdir().unwrap();
        let patches_dir = dir.path().join(".skillfile/patches/skills/my-dir");
        std::fs::create_dir_all(&patches_dir).unwrap();
        std::fs::write(patches_dir.join("tool.md.patch"), "patch").unwrap();

        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "my-dir".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "skills/my-dir".into(),
                ref_: "main".into(),
            },
        };
        let status = format_pinned_status(&entry, dir.path());
        assert!(
            status.starts_with("yes"),
            "expected 'yes ...', got: {status}"
        );
        assert!(
            has_normalized_suffix(&status, ".skillfile/patches/skills/my-dir)"),
            "expected dir patch path, got: {status}"
        );
    }

    #[test]
    fn cache_path_single_file_cached() {
        let dir = tempfile::tempdir().unwrap();
        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "foo".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "skills/foo.md".into(),
                ref_: "main".into(),
            },
        };
        let vdir = dir.path().join(".skillfile/cache/skills/foo");
        std::fs::create_dir_all(&vdir).unwrap();
        std::fs::write(vdir.join("foo.md"), "content").unwrap();

        let path = format_cache_path(&entry, dir.path());
        assert!(
            path.contains("foo.md") && !path.contains("not cached"),
            "expected cached path, got: {path}"
        );
    }

    #[test]
    fn cache_path_single_file_not_cached() {
        let dir = tempfile::tempdir().unwrap();
        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "foo".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "skills/foo.md".into(),
                ref_: "main".into(),
            },
        };
        let path = format_cache_path(&entry, dir.path());
        assert!(
            path.contains("not cached"),
            "expected 'not cached', got: {path}"
        );
    }

    #[test]
    fn cache_path_local_entry() {
        let dir = tempfile::tempdir().unwrap();
        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "foo".into(),
            source: SourceFields::Local {
                path: "skills/foo.md".into(),
            },
        };
        let path = format_cache_path(&entry, dir.path());
        assert_eq!(path, "(no cache file)");
    }

    #[test]
    fn installed_paths_no_targets() {
        let dir = tempfile::tempdir().unwrap();
        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "foo".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "skills/foo.md".into(),
                ref_: "main".into(),
            },
        };
        let manifest = Manifest {
            entries: vec![entry.clone()],
            install_targets: vec![],
        };
        let paths = format_installed_paths(&entry, &manifest, dir.path()).unwrap();
        assert_eq!(paths, vec!["(not installed)"]);
    }

    #[test]
    fn installed_paths_show_missing_secondary_target() {
        let dir = tempfile::tempdir().unwrap();
        let entry = Entry {
            entity_type: EntityType::Skill,
            name: "foo".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "skills/foo.md".into(),
                ref_: "main".into(),
            },
        };
        let manifest = Manifest {
            entries: vec![entry.clone()],
            install_targets: vec![
                InstallTarget {
                    adapter: "claude-code".into(),
                    scope: Scope::Local,
                },
                InstallTarget {
                    adapter: "copilot".into(),
                    scope: Scope::Local,
                },
            ],
        };
        let installed = dir.path().join(".claude/skills/foo");
        std::fs::create_dir_all(&installed).unwrap();
        std::fs::write(installed.join("SKILL.md"), "# Foo\n").unwrap();

        let paths = format_installed_paths(&entry, &manifest, dir.path()).unwrap();
        assert_eq!(paths.len(), 2);
        assert!(has_normalized_suffix(
            &paths[0],
            ".claude/skills/foo/SKILL.md"
        ));
        assert!(has_normalized_suffix(
            &paths[1],
            ".github/skills/foo/SKILL.md (not installed)"
        ));
    }

    #[test]
    fn installed_paths_show_flat_dir_files() {
        let dir = tempfile::tempdir().unwrap();
        let source = dir.path().join("agents/my-agent");
        std::fs::create_dir_all(&source).unwrap();
        std::fs::write(source.join("agent.md"), "# Agent\n").unwrap();
        std::fs::write(source.join("notes.md"), "# Notes\n").unwrap();

        let entry = Entry {
            entity_type: EntityType::Agent,
            name: "my-agent".into(),
            source: SourceFields::Local {
                path: "agents/my-agent".into(),
            },
        };
        let manifest = Manifest {
            entries: vec![entry.clone()],
            install_targets: vec![InstallTarget {
                adapter: "claude-code".into(),
                scope: Scope::Local,
            }],
        };
        let installed = dir.path().join(".claude/agents");
        std::fs::create_dir_all(&installed).unwrap();
        std::fs::write(installed.join("agent.md"), "# Agent\n").unwrap();
        std::fs::write(installed.join("notes.md"), "# Notes\n").unwrap();

        let paths = format_installed_paths(&entry, &manifest, dir.path()).unwrap();
        assert_eq!(paths.len(), 2);
        assert!(has_normalized_suffix(&paths[0], ".claude/agents/agent.md"));
        assert!(has_normalized_suffix(&paths[1], ".claude/agents/notes.md"));
    }

    #[test]
    fn lock_status_corrupt_lock_file() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("Skillfile.lock"), "not json").unwrap();
        let entry = Entry {
            entity_type: EntityType::Agent,
            name: "my-agent".into(),
            source: SourceFields::Github {
                owner_repo: "owner/repo".into(),
                path_in_repo: "agents/agent.md".into(),
                ref_: "main".into(),
            },
        };
        let status = format_lock_status(&entry, dir.path());
        assert!(
            status.starts_with("unknown"),
            "expected 'unknown ...', got: {status}"
        );
    }
}
