//! Cargo workspace adapter.

use async_trait::async_trait;
use cargo_metadata::{Metadata, MetadataCommand};
use command_group::CommandGroup;
use governor_application::error::{ApplicationError, ApplicationResult};
use governor_application::ports::{
    ChangelogUpdate, CommandPort, CommandReport, OwnerPackage, OwnersWorkspace, ReleasePackage,
    ReleaseWorkspace, VersionUpdate, WorkspacePort,
};
use governor_core::domain::changelog::Changelog;
use governor_core::domain::version::SemanticVersion;
use governor_owners::{PackageOwnersConfig, WorkspaceOwnersConfig};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

#[derive(Debug, Deserialize)]
struct GovernorMetadata {
    owners: Option<OwnersMetadata>,
}

#[derive(Debug, Deserialize)]
struct OwnersMetadata {
    #[serde(default)]
    users: Vec<String>,
    #[serde(default)]
    groups: HashMap<String, Vec<String>>,
}

/// Cargo workspace adapter.
#[derive(Debug, Default, Clone, Copy)]
pub struct CargoWorkspaceAdapter;

impl CargoWorkspaceAdapter {
    fn metadata(workspace_path: &Path) -> ApplicationResult<Metadata> {
        let mut command = MetadataCommand::new();
        command.current_dir(workspace_path);
        command.no_deps();
        command.exec().map_err(|error| {
            ApplicationError::Workspace(format!("failed to load cargo metadata: {error}"))
        })
    }

    fn cargo_toml_path(workspace_path: &Path) -> PathBuf {
        workspace_path.join("Cargo.toml")
    }

    fn changelog_path(workspace_path: &Path) -> PathBuf {
        workspace_path.join("CHANGELOG.md")
    }

    fn workspace_name(metadata: &Metadata) -> String {
        metadata
            .workspace_root
            .file_name()
            .map_or_else(|| "workspace".to_string(), std::string::ToString::to_string)
    }

    fn parse_workspace_version(value: &toml::Value) -> ApplicationResult<String> {
        value
            .get("workspace")
            .and_then(|workspace| workspace.get("package"))
            .and_then(|package| package.get("version"))
            .or_else(|| {
                value
                    .get("package")
                    .and_then(|package| package.get("version"))
            })
            .and_then(toml::Value::as_str)
            .map(ToOwned::to_owned)
            .ok_or_else(|| {
                ApplicationError::Workspace(
                    "no shared workspace version found in Cargo.toml".to_string(),
                )
            })
    }

    fn read_workspace_version(workspace_path: &Path) -> ApplicationResult<SemanticVersion> {
        let content = std::fs::read_to_string(Self::cargo_toml_path(workspace_path))?;
        let value: toml::Value = toml::from_str(&content).map_err(|error| {
            ApplicationError::Workspace(format!("failed to parse Cargo.toml: {error}"))
        })?;
        let version = Self::parse_workspace_version(&value)?;
        SemanticVersion::parse(&version).map_err(|error| {
            ApplicationError::Workspace(format!("failed to parse workspace version: {error}"))
        })
    }

    fn parse_owners_from_manifest(
        manifest_path: &Path,
    ) -> ApplicationResult<Option<PackageOwnersConfig>> {
        let content = std::fs::read_to_string(manifest_path)?;
        let value: toml::Value = toml::from_str(&content).map_err(|error| {
            ApplicationError::Workspace(format!(
                "failed to parse {}: {error}",
                manifest_path.display()
            ))
        })?;

        let governor = value
            .get("package")
            .and_then(|package| package.get("metadata"))
            .and_then(|metadata| metadata.get("governor"));

        let metadata: Option<GovernorMetadata> = governor
            .map(|governor| {
                governor.clone().try_into().map_err(|error| {
                    ApplicationError::Workspace(format!(
                        "failed to parse governor metadata in {}: {error}",
                        manifest_path.display()
                    ))
                })
            })
            .transpose()?;

        Ok(metadata.and_then(|meta| {
            meta.owners.map(|owners| PackageOwnersConfig {
                users: owners.users,
                groups: owners.groups.into_keys().collect(),
                ..Default::default()
            })
        }))
    }

    fn parse_workspace_owners(metadata: &Metadata) -> Option<WorkspaceOwnersConfig> {
        metadata
            .workspace_metadata
            .as_object()
            .and_then(|workspace_metadata| {
                workspace_metadata
                    .get("governor")
                    .and_then(|governor| governor.get("owners"))
                    .and_then(|owners| {
                        serde_json::from_value::<OwnersMetadata>(owners.clone()).ok()
                    })
                    .map(|owners| WorkspaceOwnersConfig {
                        users: owners.users,
                        groups: owners.groups,
                    })
            })
    }

    fn insert_changelog_section(
        existing: &str,
        version: &SemanticVersion,
        rendered: &str,
    ) -> String {
        let date = chrono::Utc::now().date_naive();
        let version_marker = format!("## [{version}]");
        let version_header = format!("## [{version}] - {date}");

        if existing.contains(&version_marker) {
            return existing.to_string();
        }

        existing.find("## [Unreleased]").map_or_else(
            || {
                let mut updated = String::new();
                updated.push_str("# Changelog\n\n## [Unreleased]\n\n");
                updated.push_str(&version_header);
                updated.push_str(rendered);
                updated.push('\n');
                updated.push_str(existing);
                updated
            },
            |unreleased_pos| {
                let after_header = existing[unreleased_pos..]
                    .find('\n')
                    .map_or(existing.len(), |offset| unreleased_pos + offset + 1);
                let mut updated = String::new();
                updated.push_str(&existing[..after_header]);
                updated.push('\n');
                updated.push_str(&version_header);
                updated.push_str(rendered);
                updated.push('\n');
                updated.push_str(&existing[after_header..]);
                updated
            },
        )
    }

    fn check_args(check: &str) -> ApplicationResult<Vec<&'static str>> {
        match check {
            "test" => Ok(vec!["cargo", "test", "--all-features"]),
            "clippy" => Ok(vec![
                "cargo",
                "clippy",
                "--all-targets",
                "--all-features",
                "--",
                "-D",
                "warnings",
            ]),
            "fmt" => Ok(vec!["cargo", "fmt", "--check"]),
            "doc" => Ok(vec!["cargo", "doc", "--no-deps", "--all-features"]),
            "build" => Ok(vec!["cargo", "build", "--all-features"]),
            other => Err(ApplicationError::InvalidArguments(format!(
                "unknown check `{other}`"
            ))),
        }
    }

    fn update_workspace_dependency_versions(value: &mut toml::Value, version: &SemanticVersion) {
        let Some(dependencies) = value
            .get_mut("workspace")
            .and_then(toml::Value::as_table_mut)
            .and_then(|workspace| workspace.get_mut("dependencies"))
            .and_then(toml::Value::as_table_mut)
        else {
            return;
        };

        for (_, dependency) in dependencies.iter_mut() {
            let Some(table) = dependency.as_table_mut() else {
                continue;
            };
            let has_local_path = table
                .get("path")
                .and_then(toml::Value::as_str)
                .is_some_and(|path| path.starts_with("crates/"));
            if has_local_path {
                table.insert(
                    "version".to_string(),
                    toml::Value::String(version.to_string()),
                );
            }
        }
    }

    fn sync_local_workspace_dependency_versions_in_text(
        rendered: &str,
        version: &SemanticVersion,
    ) -> String {
        let mut output_lines = Vec::new();
        let mut section_lines = Vec::new();
        let mut dependency_section = false;
        let mut local_dependency_section = false;

        let flush_section = |output_lines: &mut Vec<String>,
                             section_lines: &mut Vec<String>,
                             dependency_section: bool,
                             local_dependency_section: bool| {
            if dependency_section && local_dependency_section {
                for line in section_lines.iter_mut() {
                    if line.trim().starts_with("version = ") {
                        *line = format!("version = \"{version}\"");
                    }
                }
            }
            output_lines.append(section_lines);
        };

        for line in rendered.lines() {
            let trimmed = line.trim();
            if trimmed.starts_with('[') && trimmed.ends_with(']') {
                if !section_lines.is_empty() {
                    flush_section(
                        &mut output_lines,
                        &mut section_lines,
                        dependency_section,
                        local_dependency_section,
                    );
                }
                dependency_section = trimmed.starts_with("[workspace.dependencies.")
                    && trimmed != "[workspace.dependencies]";
                local_dependency_section = false;
                section_lines.push(line.to_string());
                continue;
            }

            if dependency_section && trimmed.starts_with("path = \"crates/") {
                local_dependency_section = true;
            }
            section_lines.push(line.to_string());
        }

        if !section_lines.is_empty() {
            flush_section(
                &mut output_lines,
                &mut section_lines,
                dependency_section,
                local_dependency_section,
            );
        }

        let mut output = output_lines.join("\n");
        output.push('\n');
        output
    }
}

#[async_trait]
impl WorkspacePort for CargoWorkspaceAdapter {
    fn name(&self) -> &'static str {
        "cargo_workspace"
    }

    async fn load_release_workspace(
        &self,
        workspace_path: &Path,
    ) -> ApplicationResult<ReleaseWorkspace> {
        let metadata = Self::metadata(workspace_path)?;
        let current_version = Self::read_workspace_version(workspace_path)?;
        let workspace_root = metadata.workspace_root.clone().into_std_path_buf();
        let workspace_members = metadata
            .workspace_members
            .iter()
            .map(std::string::ToString::to_string)
            .collect::<Vec<_>>();
        let packages = metadata
            .packages
            .iter()
            .filter(|package| workspace_members.contains(&package.id.to_string()))
            .map(|package| {
                let version =
                    SemanticVersion::parse(&package.version.to_string()).map_err(|error| {
                        ApplicationError::Workspace(format!(
                            "failed to parse package version {}: {error}",
                            package.version
                        ))
                    })?;

                Ok(ReleasePackage {
                    name: package.name.clone(),
                    version,
                    manifest_path: package.manifest_path.as_std_path().to_path_buf(),
                    dependencies: package
                        .dependencies
                        .iter()
                        .filter(|dependency| {
                            dependency
                                .path
                                .as_ref()
                                .is_some_and(|path| path.starts_with(&metadata.workspace_root))
                        })
                        .map(|dependency| dependency.name.clone())
                        .collect(),
                    publish: !package.publish.as_ref().is_some_and(Vec::is_empty),
                })
            })
            .collect::<ApplicationResult<Vec<_>>>()?;

        Ok(ReleaseWorkspace {
            root: workspace_root,
            name: Self::workspace_name(&metadata),
            current_version,
            packages,
        })
    }

    async fn load_owners_workspace(
        &self,
        workspace_path: &Path,
    ) -> ApplicationResult<OwnersWorkspace> {
        let metadata = Self::metadata(workspace_path)?;
        let workspace_members = metadata
            .workspace_members
            .iter()
            .map(std::string::ToString::to_string)
            .collect::<Vec<_>>();

        let packages = metadata
            .packages
            .iter()
            .filter(|package| workspace_members.contains(&package.id.to_string()))
            .map(|package| {
                Ok(OwnerPackage {
                    name: package.name.clone(),
                    owners: Self::parse_owners_from_manifest(package.manifest_path.as_std_path())?,
                })
            })
            .collect::<ApplicationResult<Vec<_>>>()?;

        Ok(OwnersWorkspace {
            root: metadata.workspace_root.clone().into_std_path_buf(),
            workspace: Self::parse_workspace_owners(&metadata),
            packages,
        })
    }

    async fn update_workspace_version(
        &self,
        workspace_path: &Path,
        version: &SemanticVersion,
        dry_run: bool,
    ) -> ApplicationResult<VersionUpdate> {
        let cargo_toml_path = Self::cargo_toml_path(workspace_path);
        let content = std::fs::read_to_string(&cargo_toml_path)?;
        let mut value: toml::Value = toml::from_str(&content).map_err(|error| {
            ApplicationError::Workspace(format!("failed to parse Cargo.toml: {error}"))
        })?;

        let version_value = toml::Value::String(version.to_string());
        if let Some(workspace) = value
            .get_mut("workspace")
            .and_then(toml::Value::as_table_mut)
        {
            if let Some(package) = workspace
                .get_mut("package")
                .and_then(toml::Value::as_table_mut)
            {
                package.insert("version".to_string(), version_value);
            }
        } else if let Some(package) = value.get_mut("package").and_then(toml::Value::as_table_mut) {
            package.insert(
                "version".to_string(),
                toml::Value::String(version.to_string()),
            );
        }
        Self::update_workspace_dependency_versions(&mut value, version);

        let mut modified_files = vec!["Cargo.toml".to_string()];
        let cargo_lock_path = workspace_path.join("Cargo.lock");
        if cargo_lock_path.exists() {
            modified_files.push("Cargo.lock".to_string());
        }

        if !dry_run {
            let rendered = toml::to_string_pretty(&value).map_err(|error| {
                ApplicationError::Workspace(format!("failed to render Cargo.toml: {error}"))
            })?;
            let rendered =
                Self::sync_local_workspace_dependency_versions_in_text(&rendered, version);
            std::fs::write(&cargo_toml_path, rendered)?;

            if cargo_lock_path.exists() {
                let output = Command::new("cargo")
                    .args(["generate-lockfile"])
                    .current_dir(workspace_path)
                    .output()?;
                if !output.status.success() {
                    return Err(ApplicationError::Workspace(format!(
                        "failed to update Cargo.lock: {}",
                        String::from_utf8_lossy(&output.stderr)
                    )));
                }
            }
        }

        Ok(VersionUpdate { modified_files })
    }

    async fn update_changelog(
        &self,
        workspace_path: &Path,
        changelog: &Changelog,
        dry_run: bool,
    ) -> ApplicationResult<ChangelogUpdate> {
        let path = Self::changelog_path(workspace_path);
        let existing = std::fs::read_to_string(&path)
            .unwrap_or_else(|_| "# Changelog\n\n## [Unreleased]\n".to_string());
        let rendered = changelog.format_keep_a_changelog();
        let updated = Self::insert_changelog_section(&existing, &changelog.version, &rendered);
        let changed = updated != existing;

        if changed && !dry_run {
            std::fs::write(&path, updated)?;
        }

        Ok(ChangelogUpdate {
            updated: changed,
            modified_files: if changed {
                vec!["CHANGELOG.md".to_string()]
            } else {
                Vec::new()
            },
        })
    }
}

#[async_trait]
impl CommandPort for CargoWorkspaceAdapter {
    fn name(&self) -> &'static str {
        "cargo_command_runner"
    }

    async fn run_check(
        &self,
        workspace_path: &Path,
        check: &str,
        capture_output: bool,
    ) -> ApplicationResult<CommandReport> {
        let args = Self::check_args(check)?;
        let mut command = Command::new(args[0]);
        command.args(&args[1..]).current_dir(workspace_path);

        let output = if capture_output {
            command.output()?
        } else {
            command
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .group_spawn()
                .map_err(|error| {
                    ApplicationError::Workspace(format!("failed to spawn {check}: {error}"))
                })?
                .wait_with_output()
                .map_err(|error| {
                    ApplicationError::Workspace(format!("failed to wait for {check}: {error}"))
                })?
        };

        Ok(CommandReport {
            name: check.to_string(),
            success: output.status.success(),
            exit_code: output.status.code(),
            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::CargoWorkspaceAdapter;
    use governor_core::domain::version::SemanticVersion;

    #[test]
    fn test_insert_changelog_section_does_not_duplicate_existing_version() {
        let version = SemanticVersion::parse("5.0.0").expect("semantic version");
        let existing = "# Changelog\n\n## [Unreleased]\n\n## [5.0.0] - 2026-04-12\n\n### Added\n- Existing entry\n";
        let rendered = "\n### Added\n- Generated entry\n";

        let updated = CargoWorkspaceAdapter::insert_changelog_section(existing, &version, rendered);

        assert_eq!(
            updated.matches("## [5.0.0]").count(),
            1,
            "existing changelog version sections must not be duplicated"
        );
        assert!(updated.contains("Existing entry"));
        assert!(!updated.contains("Generated entry"));
    }
}
