use serde::{Deserialize, Deserializer, Serialize, Serializer};

use std::collections::HashMap;

/// Type of release pipeline step.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReleaseStepType {
    Build,
    Changelog,
    Version,
    GitCommit,
    GitTag,
    GitPush,
    Changes,
    ModuleRun,
    ModuleAction(String),
}

impl ReleaseStepType {
    pub fn as_str(&self) -> &str {
        match self {
            ReleaseStepType::Build => "build",
            ReleaseStepType::Changelog => "changelog",
            ReleaseStepType::Version => "version",
            ReleaseStepType::GitCommit => "git.commit",
            ReleaseStepType::GitTag => "git.tag",
            ReleaseStepType::GitPush => "git.push",
            ReleaseStepType::Changes => "changes",
            ReleaseStepType::ModuleRun => "module.run",
            ReleaseStepType::ModuleAction(s) => s.as_str(),
        }
    }

    pub fn is_core_step(&self) -> bool {
        matches!(
            self,
            ReleaseStepType::Build
                | ReleaseStepType::Changelog
                | ReleaseStepType::Version
                | ReleaseStepType::GitCommit
                | ReleaseStepType::GitTag
                | ReleaseStepType::GitPush
                | ReleaseStepType::Changes
        )
    }
}

impl From<&str> for ReleaseStepType {
    fn from(s: &str) -> Self {
        match s {
            "build" => ReleaseStepType::Build,
            "changelog" => ReleaseStepType::Changelog,
            "version" => ReleaseStepType::Version,
            "git.commit" => ReleaseStepType::GitCommit,
            "git.tag" => ReleaseStepType::GitTag,
            "git.push" => ReleaseStepType::GitPush,
            "changes" => ReleaseStepType::Changes,
            "module.run" => ReleaseStepType::ModuleRun,
            other => ReleaseStepType::ModuleAction(other.to_string()),
        }
    }
}

impl From<String> for ReleaseStepType {
    fn from(s: String) -> Self {
        ReleaseStepType::from(s.as_str())
    }
}

impl Serialize for ReleaseStepType {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for ReleaseStepType {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Ok(ReleaseStepType::from(s))
    }
}

use crate::component::{self, Component};
use crate::core::local_files::FileSystem;
use crate::error::{Error, Result};
use crate::module::{self, ModuleManifest};
use crate::pipeline::{
    self, PipelineCapabilityResolver, PipelinePlanStep, PipelineRunResult, PipelineRunStatus,
    PipelineStep, PipelineStepExecutor, PipelineStepResult,
};
use crate::{changelog, version};

fn parse_module_inputs(values: &[serde_json::Value]) -> Result<Vec<(String, String)>> {
    let mut inputs = Vec::new();
    for value in values {
        let entry = value.as_object().ok_or_else(|| {
            Error::validation_invalid_argument(
                "release.steps",
                "module.run inputs must be objects with 'id' and 'value'",
                None,
                None,
            )
        })?;
        let id = entry.get("id").and_then(|v| v.as_str()).ok_or_else(|| {
            Error::validation_invalid_argument(
                "release.steps",
                "module.run inputs require 'id'",
                None,
                None,
            )
        })?;
        let value = entry.get("value").and_then(|v| v.as_str()).ok_or_else(|| {
            Error::validation_invalid_argument(
                "release.steps",
                "module.run inputs require 'value'",
                None,
                None,
            )
        })?;
        inputs.push((id.to_string(), value.to_string()));
    }

    Ok(inputs)
}

fn parse_module_args(values: &[serde_json::Value]) -> Result<Vec<String>> {
    let mut args = Vec::new();
    for value in values {
        let arg = value.as_str().ok_or_else(|| {
            Error::validation_invalid_argument(
                "release.steps",
                "module.run args must be strings",
                None,
                None,
            )
        })?;
        args.push(arg.to_string());
    }
    Ok(args)
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]

pub struct ReleaseConfig {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub enabled: Option<bool>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub steps: Vec<ReleaseStep>,
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub settings: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]

pub struct ReleaseStep {
    pub id: String,
    #[serde(rename = "type")]
    pub step_type: ReleaseStepType,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub needs: Vec<String>,
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub config: HashMap<String, serde_json::Value>,
}

impl From<ReleaseStep> for PipelineStep {
    fn from(step: ReleaseStep) -> Self {
        PipelineStep {
            id: step.id,
            step_type: step.step_type.as_str().to_string(),
            label: step.label,
            needs: step.needs,
            config: step.config,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]

pub struct ReleasePlan {
    pub component_id: String,
    pub enabled: bool,
    pub steps: Vec<ReleasePlanStep>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub warnings: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub hints: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]

pub struct ReleaseRun {
    pub component_id: String,
    pub enabled: bool,
    pub result: PipelineRunResult,
}

#[derive(Debug, Clone, Serialize, Deserialize)]

pub struct ReleaseArtifact {
    pub path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub artifact_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub platform: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]

struct ReleaseContext {
    version: Option<String>,
    tag: Option<String>,
    notes: Option<String>,
    artifacts: Vec<ReleaseArtifact>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]

pub struct ReleasePlanStep {
    pub id: String,
    #[serde(rename = "type")]
    pub step_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub needs: Vec<String>,
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub config: HashMap<String, serde_json::Value>,
    pub status: ReleasePlanStatus,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub missing: Vec<String>,
}

impl From<PipelinePlanStep> for ReleasePlanStep {
    fn from(step: PipelinePlanStep) -> Self {
        let status = match step.status {
            pipeline::PipelineStepStatus::Ready => ReleasePlanStatus::Ready,
            pipeline::PipelineStepStatus::Missing => ReleasePlanStatus::Missing,
            pipeline::PipelineStepStatus::Disabled => ReleasePlanStatus::Disabled,
        };

        Self {
            id: step.id,
            step_type: step.step_type,
            label: step.label,
            needs: step.needs,
            config: step.config,
            status,
            missing: step.missing,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]

pub enum ReleasePlanStatus {
    Ready,
    Missing,
    Disabled,
}

struct ReleaseCapabilityResolver {
    modules: Vec<ModuleManifest>,
}

impl ReleaseCapabilityResolver {
    fn new(modules: Vec<ModuleManifest>) -> Self {
        Self { modules }
    }
}

impl PipelineCapabilityResolver for ReleaseCapabilityResolver {
    fn is_supported(&self, step_type: &str) -> bool {
        let st = ReleaseStepType::from(step_type);
        st == ReleaseStepType::ModuleRun
            || st.is_core_step()
            || self.supports_module_action(step_type)
    }

    fn missing(&self, step_type: &str) -> Vec<String> {
        if ReleaseStepType::from(step_type) == ReleaseStepType::ModuleRun {
            return Vec::new();
        }
        let action_id = format!("release.{}", step_type);
        vec![format!("Missing action '{}'", action_id)]
    }
}

impl ReleaseCapabilityResolver {
    fn supports_module_action(&self, step_type: &str) -> bool {
        let action_id = format!("release.{}", step_type);
        self.modules
            .iter()
            .any(|module| module.actions.iter().any(|action| action.id == action_id))
    }
}

struct ReleaseStepExecutor {
    component_id: String,
    modules: Vec<ModuleManifest>,
    context: std::sync::Mutex<ReleaseContext>,
}

impl ReleaseStepExecutor {
    fn new(component_id: String, modules: Vec<ModuleManifest>) -> Self {
        Self {
            component_id,
            modules,
            context: std::sync::Mutex::new(ReleaseContext::default()),
        }
    }

    fn step_result(
        &self,
        step: &PipelineStep,
        status: PipelineRunStatus,
        data: Option<serde_json::Value>,
        error: Option<String>,
        hints: Vec<crate::error::Hint>,
    ) -> PipelineStepResult {
        PipelineStepResult {
            id: step.id.clone(),
            step_type: step.step_type.clone(),
            status,
            missing: Vec::new(),
            warnings: Vec::new(),
            hints,
            data,
            error,
        }
    }

    fn execute_core_step(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let step_type = ReleaseStepType::from(step.step_type.as_str());
        match step_type {
            ReleaseStepType::Build => self.run_build(step),
            ReleaseStepType::Changes => self.run_changes(step),
            ReleaseStepType::Version => self.run_version(step),
            ReleaseStepType::GitCommit => self.run_git_commit(step),
            ReleaseStepType::GitTag => self.run_git_tag(step),
            ReleaseStepType::GitPush => self.run_git_push(step),
            _ => Err(Error::validation_invalid_argument(
                "release.steps",
                format!("Unsupported core step '{}'", step.step_type),
                None,
                None,
            )),
        }
    }

    fn run_build(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let (output, exit_code) = crate::build::run(&self.component_id)?;
        let data = serde_json::to_value(output)
            .map_err(|e| Error::internal_json(e.to_string(), Some("build output".to_string())))?;
        let status = if exit_code == 0 {
            PipelineRunStatus::Success
        } else {
            PipelineRunStatus::Failed
        };
        Ok(self.step_result(step, status, Some(data), None, Vec::new()))
    }

    fn run_changes(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let include_diff = step
            .config
            .get("includeDiff")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        let output = crate::git::changes(Some(&self.component_id), None, include_diff)?;
        let data = serde_json::to_value(output)
            .map_err(|e| Error::internal_json(e.to_string(), Some("changes output".to_string())))?;
        Ok(self.step_result(
            step,
            PipelineRunStatus::Success,
            Some(data),
            None,
            Vec::new(),
        ))
    }

    fn run_version(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let mode = step
            .config
            .get("mode")
            .and_then(|v| v.as_str())
            .unwrap_or("bump");

        match mode {
            "validate" => self.run_version_validate(step),
            _ => self.run_version_bump(step),
        }
    }

    fn run_version_bump(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let bump_type = step
            .config
            .get("bump")
            .and_then(|v| v.as_str())
            .unwrap_or("patch");
        let result = version::bump_version(Some(&self.component_id), bump_type)?;
        let data = serde_json::to_value(&result)
            .map_err(|e| Error::internal_json(e.to_string(), Some("version output".to_string())))?;
        self.store_version_context(&result.new_version)?;
        Ok(self.step_result(
            step,
            PipelineRunStatus::Success,
            Some(data),
            None,
            Vec::new(),
        ))
    }

    fn run_version_validate(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let info = version::read_version(Some(&self.component_id))?;
        let data = serde_json::to_value(&info)
            .map_err(|e| Error::internal_json(e.to_string(), Some("version output".to_string())))?;
        self.store_version_context(&info.version)?;
        Ok(self.step_result(
            step,
            PipelineRunStatus::Success,
            Some(data),
            None,
            Vec::new(),
        ))
    }

    fn run_git_tag(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let tag = step
            .config
            .get("name")
            .and_then(|v| v.as_str())
            .map(|v| v.to_string())
            .or_else(|| {
                step.config
                    .get("versionTag")
                    .and_then(|v| v.as_str())
                    .map(|v| v.to_string())
            });

        let tag_name = match tag {
            Some(name) => name,
            None => self.default_tag()?,
        };

        let message = step
            .config
            .get("message")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string())
            .unwrap_or_else(|| format!("Release {}", tag_name));

        let output = crate::git::tag(Some(&self.component_id), Some(&tag_name), Some(&message))?;
        let data = serde_json::to_value(&output)
            .map_err(|e| Error::internal_json(e.to_string(), Some("git tag output".to_string())))?;

        if !output.success {
            let mut hints = Vec::new();

            if output.stderr.contains("already exists") {
                let component = component::load(&self.component_id)?;
                let local_exists = crate::git::tag_exists_locally(&component.local_path, &tag_name)
                    .unwrap_or(false);
                let remote_exists =
                    crate::git::tag_exists_on_remote(&component.local_path, &tag_name)
                        .unwrap_or(false);

                if local_exists && !remote_exists {
                    hints.push(crate::error::Hint {
                        message: format!(
                            "Tag '{}' exists locally but not on remote. Push it with: git push origin {}",
                            tag_name, tag_name
                        ),
                    });
                } else if local_exists && remote_exists {
                    hints.push(crate::error::Hint {
                        message: format!(
                            "Tag '{}' already exists locally and on remote. Delete local tag first: git tag -d {}",
                            tag_name, tag_name
                        ),
                    });
                }
            }

            return Ok(self.step_result(
                step,
                PipelineRunStatus::Failed,
                Some(data),
                Some(output.stderr),
                hints,
            ));
        }

        self.store_tag_context(&tag_name)?;
        Ok(self.step_result(
            step,
            PipelineRunStatus::Success,
            Some(data),
            None,
            Vec::new(),
        ))
    }

    fn run_git_push(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let tags = step
            .config
            .get("tags")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        let output = crate::git::push(Some(&self.component_id), tags)?;
        let data = serde_json::to_value(output).map_err(|e| {
            Error::internal_json(e.to_string(), Some("git push output".to_string()))
        })?;
        Ok(self.step_result(
            step,
            PipelineRunStatus::Success,
            Some(data),
            None,
            Vec::new(),
        ))
    }

    fn run_git_commit(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let status_output = crate::git::status(Some(&self.component_id))?;
        let is_clean = status_output.stdout.trim().is_empty();

        if is_clean {
            let data = serde_json::json!({
                "skipped": true,
                "reason": "working tree is clean, nothing to commit"
            });
            return Ok(self.step_result(
                step,
                PipelineRunStatus::Success,
                Some(data),
                None,
                Vec::new(),
            ));
        }

        let should_amend = self.should_amend_release_commit()?;

        let message = step
            .config
            .get("message")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string())
            .unwrap_or_else(|| self.default_commit_message());

        let options = crate::git::CommitOptions {
            staged_only: false,
            files: None,
            exclude: None,
            amend: should_amend,
        };

        let output = crate::git::commit(Some(&self.component_id), Some(&message), options)?;
        let mut data = serde_json::to_value(&output).map_err(|e| {
            Error::internal_json(e.to_string(), Some("git commit output".to_string()))
        })?;

        if should_amend {
            data["amended"] = serde_json::json!(true);
        }

        let status = if output.success {
            PipelineRunStatus::Success
        } else {
            PipelineRunStatus::Failed
        };

        Ok(self.step_result(step, status, Some(data), None, Vec::new()))
    }

    fn default_commit_message(&self) -> String {
        let context = self.context.lock().ok();
        let version = context
            .as_ref()
            .and_then(|c| c.version.as_ref())
            .map(|v| v.as_str())
            .unwrap_or("unknown");
        format!("release: v{}", version)
    }

    fn should_amend_release_commit(&self) -> Result<bool> {
        let component = component::load(&self.component_id)?;

        let log_output = crate::git::execute_git_for_release(
            &component.local_path,
            &["log", "-1", "--format=%s"],
        )
        .map_err(|e| Error::other(e.to_string()))?;
        if !log_output.status.success() {
            return Ok(false);
        }
        let last_message = String::from_utf8_lossy(&log_output.stdout)
            .trim()
            .to_string();

        if !last_message.starts_with("release: v") {
            return Ok(false);
        }

        let status_output =
            crate::git::execute_git_for_release(&component.local_path, &["status", "-sb"])
                .map_err(|e| Error::other(e.to_string()))?;
        if !status_output.status.success() {
            return Ok(false);
        }
        let status_str = String::from_utf8_lossy(&status_output.stdout);
        let is_ahead = status_str.contains("[ahead");

        Ok(is_ahead)
    }

    fn build_release_payload(&self, step: &PipelineStep) -> Result<serde_json::Value> {
        let component = component::load(&self.component_id)?;
        let context = self.context.lock().map_err(|_| {
            Error::internal_unexpected("Failed to lock release context".to_string())
        })?;
        let version = context.version.clone().unwrap_or_default();
        let tag = context
            .tag
            .clone()
            .unwrap_or_else(|| format!("v{}", version));
        let notes = context.notes.clone().unwrap_or_default();
        let artifacts = context.artifacts.clone();

        let release_payload = serde_json::json!({
            "release": {
                "version": version,
                "tag": tag,
                "notes": notes,
                "component_id": self.component_id,
                "local_path": component.local_path,
                "artifacts": artifacts
            }
        });

        let mut payload = release_payload;
        if !step.config.is_empty() {
            let config_value = serde_json::to_value(&step.config).map_err(|e| {
                Error::internal_json(e.to_string(), Some("release step config".to_string()))
            })?;
            payload["config"] = config_value;
        }

        Ok(payload)
    }

    fn store_version_context(&self, version_value: &str) -> Result<()> {
        let mut context = self.context.lock().map_err(|_| {
            Error::internal_unexpected("Failed to lock release context".to_string())
        })?;
        context.version = Some(version_value.to_string());
        context.tag = Some(format!("v{}", version_value));
        context.notes = Some(self.load_release_notes()?);
        Ok(())
    }

    fn store_tag_context(&self, tag_value: &str) -> Result<()> {
        let mut context = self.context.lock().map_err(|_| {
            Error::internal_unexpected("Failed to lock release context".to_string())
        })?;
        context.tag = Some(tag_value.to_string());
        Ok(())
    }

    fn default_tag(&self) -> Result<String> {
        let context = self.context.lock().map_err(|_| {
            Error::internal_unexpected("Failed to lock release context".to_string())
        })?;
        if let Some(tag) = context.tag.as_ref() {
            return Ok(tag.clone());
        }
        if let Some(version) = context.version.as_ref() {
            return Ok(format!("v{}", version));
        }
        let info = version::read_version(Some(&self.component_id))?;
        Ok(format!("v{}", info.version))
    }

    fn load_release_notes(&self) -> Result<String> {
        let component = component::load(&self.component_id)?;
        let changelog_path = changelog::resolve_changelog_path(&component)?;
        let changelog_content = crate::core::local_files::local().read(&changelog_path)?;
        let notes = extract_latest_notes(&changelog_content).ok_or_else(|| {
            Error::validation_invalid_argument(
                "changelog",
                "No finalized changelog entries found for release notes",
                None,
                None,
            )
        })?;
        Ok(notes)
    }

    fn update_artifacts_from_step(
        &self,
        step: &PipelineStep,
        response: &serde_json::Value,
    ) -> Result<()> {
        if !matches!(ReleaseStepType::from(step.step_type.as_str()), ReleaseStepType::ModuleAction(ref s) if s == "package") {
            return Ok(());
        }

        let artifacts_value = match response.get("artifacts") {
            Some(value) => Some(value.clone()),
            None => response
                .get("stdout")
                .and_then(|value| value.as_str())
                .and_then(|stdout| serde_json::from_str::<serde_json::Value>(stdout).ok()),
        };
        let Some(artifacts_value) = artifacts_value else {
            return Ok(());
        };

        let artifacts = parse_release_artifacts(&artifacts_value)?;
        if artifacts.is_empty() {
            return Ok(());
        }

        let mut context = self.context.lock().map_err(|_| {
            Error::internal_unexpected("Failed to lock release context".to_string())
        })?;
        context.artifacts = artifacts;
        Ok(())
    }

    fn run_module_action(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let action_id = format!("release.{}", step.step_type);
        let modules = resolve_module_actions(&self.modules, &action_id)?;
        let payload = self.build_release_payload(step)?;

        let mut results = Vec::new();
        for module in &modules {
            let response =
                module::execute_action(&module.id, &action_id, None, None, Some(&payload))?;
            let module_data = serde_json::to_value(&response).map_err(|e| {
                Error::internal_json(e.to_string(), Some("module action output".to_string()))
            })?;
            self.update_artifacts_from_step(step, &module_data)?;
            results.push(serde_json::json!({
                "module": module.id,
                "response": module_data
            }));
        }

        let data = serde_json::json!({
            "action": action_id,
            "results": results
        });

        Ok(self.step_result(
            step,
            PipelineRunStatus::Success,
            Some(data),
            None,
            Vec::new(),
        ))
    }

    fn run_module_runtime(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let module_id = step
            .config
            .get("module")
            .and_then(|v| v.as_str())
            .ok_or_else(|| {
                Error::validation_invalid_argument(
                    "release.steps",
                    "module.run requires config.module",
                    None,
                    None,
                )
            })?;

        let inputs = step
            .config
            .get("inputs")
            .and_then(|v| v.as_array())
            .map(|values| parse_module_inputs(values))
            .unwrap_or_else(|| Ok(Vec::new()))?;
        let args = step
            .config
            .get("args")
            .and_then(|v| v.as_array())
            .map(|values| parse_module_args(values))
            .unwrap_or_else(|| Ok(Vec::new()))?;

        let payload = self.build_release_payload(step)?;
        let working_dir = payload
            .get("release")
            .and_then(|r| r.get("local_path"))
            .and_then(|p| p.as_str());

        let outcome = module::run_module_runtime(
            module_id,
            None,
            None,
            inputs,
            args,
            Some(&payload),
            working_dir,
        )?;

        let data = serde_json::json!({
            "module": module_id,
            "stdout": outcome.result.stdout,
            "stderr": outcome.result.stderr,
            "exitCode": outcome.result.exit_code,
            "success": outcome.result.success,
            "payload": payload
        });

        let status = if outcome.result.success {
            PipelineRunStatus::Success
        } else {
            PipelineRunStatus::Failed
        };

        Ok(self.step_result(step, status, Some(data), None, Vec::new()))
    }
}

impl PipelineStepExecutor for ReleaseStepExecutor {
    fn execute_step(&self, step: &PipelineStep) -> Result<PipelineStepResult> {
        let step_type = ReleaseStepType::from(step.step_type.as_str());

        if step_type.is_core_step() {
            return self.execute_core_step(step);
        }

        if step_type == ReleaseStepType::ModuleRun {
            return self.run_module_runtime(step);
        }

        self.run_module_action(step)
    }
}

fn resolve_modules(component: &Component, module_id: Option<&str>) -> Result<Vec<ModuleManifest>> {
    if module_id.is_some() {
        return Err(Error::validation_invalid_argument(
            "module",
            "Module selection is configured via component.modules; --module is not supported",
            None,
            None,
        ));
    }

    let mut modules = Vec::new();
    if let Some(configured) = component.modules.as_ref() {
        let mut module_ids: Vec<String> = configured.keys().cloned().collect();
        module_ids.sort();
        let suggestions = module::available_module_ids();
        for module_id in module_ids {
            let manifest = module::load_module(&module_id).map_err(|_| {
                Error::module_not_found(module_id.to_string(), suggestions.clone())
            })?;
            modules.push(manifest);
        }
    }

    Ok(modules)
}

fn resolve_module_actions(
    modules: &[ModuleManifest],
    action_id: &str,
) -> Result<Vec<ModuleManifest>> {
    let matches: Vec<ModuleManifest> = modules
        .iter()
        .filter(|module| module.actions.iter().any(|action| action.id == action_id))
        .cloned()
        .collect();

    if matches.is_empty() {
        return Err(Error::validation_invalid_argument(
            "release.steps",
            format!("No module provides action '{}'", action_id),
            None,
            None,
        ));
    }

    Ok(matches)
}

fn extract_latest_notes(content: &str) -> Option<String> {
    let mut in_section = false;
    let mut buffer = Vec::new();

    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("## ") {
            if in_section {
                break;
            }
            if extract_version_from_heading(trimmed).is_some() {
                in_section = true;
                continue;
            }
        }

        if in_section {
            buffer.push(line);
        }
    }

    let notes = buffer.join("\n").trim().to_string();
    if notes.is_empty() {
        None
    } else {
        Some(notes)
    }
}

fn extract_version_from_heading(label: &str) -> Option<String> {
    let semver_pattern = regex::Regex::new(r"\[?(\d+\.\d+\.\d+)\]?").ok()?;
    semver_pattern
        .captures(label)
        .and_then(|caps| caps.get(1))
        .map(|m| m.as_str().to_string())
}

fn parse_release_artifacts(value: &serde_json::Value) -> Result<Vec<ReleaseArtifact>> {
    let mut artifacts = Vec::new();
    let items = match value {
        serde_json::Value::Array(arr) => arr.clone(),
        serde_json::Value::Object(_) => vec![value.clone()],
        _ => Vec::new(),
    };

    for item in items {
        let artifact = match item {
            serde_json::Value::String(path) => ReleaseArtifact {
                path,
                artifact_type: None,
                platform: None,
            },
            serde_json::Value::Object(map) => {
                let path = map
                    .get("path")
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| {
                        Error::validation_invalid_argument(
                            "release.artifacts",
                            "Artifact is missing 'path'",
                            None,
                            None,
                        )
                    })?
                    .to_string();
                let artifact_type = map
                    .get("type")
                    .and_then(|v| v.as_str())
                    .map(|v| v.to_string());
                let platform = map
                    .get("platform")
                    .and_then(|v| v.as_str())
                    .map(|v| v.to_string());
                ReleaseArtifact {
                    path,
                    artifact_type,
                    platform,
                }
            }
            _ => {
                return Err(Error::validation_invalid_argument(
                    "release.artifacts",
                    "Artifact entry is invalid",
                    None,
                    None,
                ))
            }
        };
        artifacts.push(artifact);
    }

    Ok(artifacts)
}

pub fn resolve_component_release(component: &Component) -> Option<ReleaseConfig> {
    component.release.clone()
}

fn validate_plan_prerequisites(component: &Component) -> Vec<String> {
    use crate::core::local_files::FileSystem;
    let mut warnings = Vec::new();

    // Check changelog status
    match changelog::resolve_changelog_path(component) {
        Ok(changelog_path) => {
            if let Ok(content) = crate::core::local_files::local().read(&changelog_path) {
                let settings = changelog::resolve_effective_settings(Some(component));
                if let Ok(Some(status)) =
                    changelog::check_next_section_content(&content, &settings.next_section_aliases)
                {
                    match status.as_str() {
                        "empty" => {
                            warnings.push(
                                "No unreleased changelog entries. Run `homeboy changelog add` first."
                                    .to_string(),
                            );
                        }
                        "subsection_headers_only" => {
                            warnings.push(
                                "Changelog has subsection headers but no items. Add entries with `homeboy changelog add`."
                                    .to_string(),
                            );
                        }
                        _ => {}
                    }
                }
            }
        }
        Err(_) => {
            warnings.push("No changelog configured for this component.".to_string());
        }
    }

    warnings
}

pub fn plan(component_id: &str, module_id: Option<&str>) -> Result<ReleasePlan> {
    let component = component::load(component_id)?;
    let modules = resolve_modules(&component, module_id)?;
    let resolver = ReleaseCapabilityResolver::new(modules.clone());
    let release = resolve_component_release(&component).ok_or_else(|| {
        Error::validation_invalid_argument(
            "release",
            "Release configuration is missing",
            Some(component_id.to_string()),
            None,
        )
        .with_hint(format!(
            "Use 'homeboy component set {} --json' to add a release block",
            component_id
        ))
        .with_hint("See 'homeboy docs commands/release' for examples")
    })?;

    let enabled = release.enabled.unwrap_or(true);

    let (release_steps, commit_auto_inserted) = auto_insert_commit_step(release.steps);
    let pipeline_steps: Vec<PipelineStep> = release_steps
        .iter()
        .cloned()
        .map(PipelineStep::from)
        .collect();
    let pipeline_plan = pipeline::plan(&pipeline_steps, &resolver, enabled, "release.steps")?;
    let steps: Vec<ReleasePlanStep> = pipeline_plan
        .steps
        .into_iter()
        .map(ReleasePlanStep::from)
        .collect();

    // Validate plan prerequisites and merge warnings
    let mut warnings = pipeline_plan.warnings;
    warnings.extend(validate_plan_prerequisites(&component));

    let mut hints = build_plan_hints(component_id, &steps, &modules);
    if commit_auto_inserted {
        hints.insert(
            0,
            "git.commit step auto-inserted before git.tag".to_string(),
        );
    }
    hints.push(format!(
        "Review changes first with: homeboy changes {}",
        component_id
    ));

    Ok(ReleasePlan {
        component_id: component_id.to_string(),
        enabled,
        steps,
        warnings,
        hints,
    })
}

pub fn run(component_id: &str, module_id: Option<&str>) -> Result<ReleaseRun> {
    let component = component::load(component_id)?;
    let modules = resolve_modules(&component, module_id)?;
    let resolver = ReleaseCapabilityResolver::new(modules.clone());
    let release = resolve_component_release(&component).ok_or_else(|| {
        Error::validation_invalid_argument(
            "release",
            "Release configuration is missing",
            Some(component_id.to_string()),
            None,
        )
        .with_hint(format!(
            "Use 'homeboy component set {} --json' to add a release block",
            component_id
        ))
        .with_hint("See 'homeboy docs commands/release' for examples")
    })?;

    let enabled = release.enabled.unwrap_or(true);

    let (release_steps, _commit_auto_inserted) = auto_insert_commit_step(release.steps);

    validate_preflight(&component, &release_steps)?;

    let executor = ReleaseStepExecutor::new(component_id.to_string(), modules.clone());

    let pipeline_steps: Vec<PipelineStep> =
        release_steps.into_iter().map(PipelineStep::from).collect();

    let run_result = pipeline::run(
        &pipeline_steps,
        std::sync::Arc::new(executor),
        std::sync::Arc::new(resolver),
        enabled,
        "release.steps",
    )?;

    Ok(ReleaseRun {
        component_id: component_id.to_string(),
        enabled,
        result: run_result,
    })
}

fn validate_preflight(component: &Component, steps: &[ReleaseStep]) -> Result<()> {
    let uncommitted = crate::git::get_uncommitted_changes(&component.local_path)?;
    let has_commit_step = steps.iter().any(|s| s.step_type == ReleaseStepType::GitCommit);

    if uncommitted.has_changes && !has_commit_step {
        return Err(Error::validation_invalid_argument(
            "working_tree",
            "Working tree has uncommitted changes",
            None,
            None,
        )
        .with_hint(
            "Commit your changes first with `git commit` or ensure a `git.commit` step \
             is in your release pipeline (auto-inserted when git.tag is present).",
        ));
    }

    // Validate changelog has no unreleased section with content
    if let Ok(changelog_path) = crate::changelog::resolve_changelog_path(component) {
        let changelog_content = crate::core::local_files::local().read(&changelog_path);
        if let Ok(content) = changelog_content {
            let settings = crate::changelog::resolve_effective_settings(Some(component));
            if let Some(status) = crate::changelog::check_next_section_content(
                &content,
                &settings.next_section_aliases,
            )? {
                match status.as_str() {
                    "empty" => {
                        // Empty unreleased section is fine - no content to release
                    }
                    "subsection_headers_only" | _ => {
                        // Has unreleased content - should be finalized before release
                        return Err(Error::validation_invalid_argument(
                            "changelog",
                            "Changelog has unreleased section with content. Finalize changelog before releasing.",
                            None,
                            Some(vec![
                                "Run `homeboy version bump <component>` to finalize and increment version".to_string(),
                                "Or run `homeboy changelog add <component> -m \"...\"` to add more items".to_string(),
                            ]),
                        ));
                    }
                }
            }
        }
    }

    Ok(())
}

fn auto_insert_commit_step(steps: Vec<ReleaseStep>) -> (Vec<ReleaseStep>, bool) {
    let has_tag = steps.iter().any(|s| s.step_type == ReleaseStepType::GitTag);
    let has_commit = steps.iter().any(|s| s.step_type == ReleaseStepType::GitCommit);

    if !has_tag || has_commit {
        return (steps, false);
    }

    let mut result = Vec::with_capacity(steps.len() + 1);
    let mut inserted = false;

    for step in steps {
        if step.step_type == ReleaseStepType::GitTag && !inserted {
            let commit_step = ReleaseStep {
                id: "git.commit".to_string(),
                step_type: ReleaseStepType::GitCommit,
                label: Some("Commit release changes".to_string()),
                needs: step.needs.clone(),
                config: HashMap::new(),
            };
            result.push(commit_step);
            inserted = true;

            let mut tag_step = step;
            tag_step.needs = vec!["git.commit".to_string()];
            result.push(tag_step);
        } else {
            result.push(step);
        }
    }

    (result, inserted)
}

fn build_plan_hints(
    component_id: &str,
    steps: &[ReleasePlanStep],
    modules: &[ModuleManifest],
) -> Vec<String> {
    let mut hints = Vec::new();
    if steps.is_empty() {
        hints.push("Release plan has no steps".to_string());
    }

    if steps
        .iter()
        .any(|step| matches!(step.status, ReleasePlanStatus::Missing))
    {
        if modules.is_empty() {
            hints.push("Configure component modules to resolve release actions".to_string());
        } else {
            let module_names: Vec<String> =
                modules.iter().map(|module| module.id.clone()).collect();
            hints.push(format!(
                "Release actions are resolved from modules: {}",
                module_names.join(", ")
            ));
        }
    }

    if !hints.is_empty() {
        hints.push(format!(
            "Update release config with: homeboy component set {} --json",
            component_id
        ));
    }

    hints
}
