//! South — a multi-repo dependency manager.
//!
//! South manages dependencies across multiple Git repositories using a YAML manifest
//! file (`south.yaml`). It is inspired by Zephyr RTOS's `west`, but simpler and
//! agnostic to what the project is for.
//!
//! # Workspace layout
//!
//! A south workspace is a directory containing your main project repo (with `south.yaml`)
//! alongside cloned dependency repositories:
//!
//! ```text
//! my-workspace/
//!     my-project/
//!         south.yaml
//!     dep-repo/
//!     libs/another-dep/
//! ```

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

use serde::{Deserialize, Serialize};

use crate::error::{SouthError, SouthResult};

pub mod error;
pub mod git;

/// A single repository entry in the south manifest.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ManifestRepo {
    /// Name of the repository
    pub name: String,

    /// URL to repository remote
    pub url: String,

    /// Revision: Can be git tag, branch,
    /// pull request or commit hash
    pub revision: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    /// Optional path to clone repository to, relative to
    /// manifest project-repo's parent directory. If None,
    /// the repo is placed in the south workspace
    /// Example:
    /// south-workspace
    ///     |____ path/to/repo1
    ///     |____ path/to/repo2
    ///     |____ project-repo/south.yaml
    pub path: Option<String>,
}

impl ManifestRepo {
    /// Create a new repository entry.
    pub fn new(name: String, url: String, revision: String, path: Option<String>) -> Self {
        ManifestRepo {
            name,
            url,
            revision,
            path,
        }
    }
}

/// The south manifest, representing the contents of `south.yaml`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SouthManifest {
    pub repos: Vec<ManifestRepo>,
}

impl SouthManifest {
    pub fn new() -> Self {
        SouthManifest { repos: Vec::new() }
    }

    /// Create a new `south.yaml` file in the given project directory.
    pub fn create_south_manifest(&self, project_dir: &Path) -> SouthResult<()> {
        let yaml = serde_yaml::to_string(self)
            .map_err(|e| SouthError::IoError(e.to_string()))?;

        std::fs::write(project_dir.join("south.yaml"), yaml)
            .map_err(|e| SouthError::IoError(e.to_string()))?;

        println!("Created south.yaml in {:?} ✨ 🍰 ✨", project_dir);
        println!("You can now add repositories your project depends on with `south add`");

        Ok(())
    }

    /// Load a manifest from the `south.yaml` file in the given directory.
    pub fn load(project_dir: &Path) -> SouthResult<Self> {
        let contents = std::fs::read_to_string(project_dir.join("south.yaml"))
            .map_err(|e| SouthError::IoError(e.to_string()))?;
        let manifest: SouthManifest = serde_yaml::from_str(&contents)
            .map_err(|e| SouthError::ConfigurationError(format!("Failed to parse south.yaml: {}", e)))?;
        Ok(manifest)
    }

    /// Save the manifest back to `south.yaml` in the given directory.
    pub fn save(&self, project_dir: &Path) -> SouthResult<()> {
        let yaml = serde_yaml::to_string(self)
            .map_err(|e| SouthError::IoError(e.to_string()))?;
        std::fs::write(project_dir.join("south.yaml"), yaml)
            .map_err(|e| SouthError::IoError(e.to_string()))?;
        Ok(())
    }
}

/// Builder for constructing a [`SouthManifest`] programmatically.
pub struct SouthManifestBuilder {
    manifest: SouthManifest,
}

impl SouthManifestBuilder {
    pub fn new() -> Self {
        SouthManifestBuilder {
            manifest: SouthManifest::new(),
        }
    }

    pub fn add_repo(mut self, repo: ManifestRepo) -> Self {
        self.manifest.repos.push(repo);
        self
    }

    pub fn finish(self) -> SouthManifest {
        self.manifest
    }
}

/// Walk up from `start_dir` looking for `south.yaml`.
/// Returns the directory containing `south.yaml` (the project repo root).
pub fn find_manifest_dir(start_dir: &Path) -> SouthResult<PathBuf> {
    let mut dir = start_dir;
    loop {
        if dir.join("south.yaml").exists() {
            return Ok(dir.to_path_buf());
        }
        dir = match dir.parent() {
            Some(p) => p,
            None => {
                return Err(SouthError::ConfigurationError(
                    "south.yaml not found in any parent directory".to_string(),
                ))
            }
        }
    }
}

/// Returns the workspace root (parent of the manifest directory).
/// This is where dependency repos get cloned.
pub fn find_workspace_root(start_dir: &Path) -> SouthResult<PathBuf> {
    let manifest_dir = find_manifest_dir(start_dir)?;
    manifest_dir
        .parent()
        .map(|p| p.to_path_buf())
        .ok_or_else(|| {
            SouthError::ConfigurationError(
                "Manifest directory has no parent (cannot determine workspace root)".to_string(),
            )
        })
}

/// Extract a repository name from a git URL.
/// Handles both SSH (`git@github.com:user/repo.git`) and
/// HTTPS (`https://github.com/user/repo`) URLs.
pub fn repo_name_from_url(url: &str) -> String {
    let cleaned = url.trim_end_matches('/');
    let without_git = cleaned.trim_end_matches(".git");
    without_git
        .rsplit(|c| c == '/' || c == ':')
        .next()
        .unwrap_or("unknown")
        .to_string()
}
