//! Pipeline stage dispatch: assign free coder and QA agents to stories in `2_current/` and `3_qa/`.

use std::path::Path;

use crate::config::ProjectConfig;
use crate::pipeline_state::Stage;
use crate::slog;

use super::super::super::PipelineStage;
use super::super::AgentPool;
use super::scan::{
    count_active_agents_for_stage, find_free_agent_for_stage, is_agent_free,
    is_story_assigned_for_stage, scan_stage_items,
};
use super::story_checks::{
    has_review_hold, has_unmet_dependencies, is_story_blocked, is_story_frozen,
    read_story_front_matter_agent,
};

impl AgentPool {
    /// Assign free agents to stories in the coder (`2_current/`) and QA (`3_qa/`) stages.
    ///
    /// For each stage, iterates over pending stories and starts a free agent if one is
    /// available.  Respects `max_coders`, `review_hold`, frozen, blocked, and unmet-dep
    /// guards.  Agent front-matter preferences and stage-mismatch fallback are handled
    /// here as well.
    pub(super) async fn assign_pipeline_stages(&self, project_root: &Path, config: &ProjectConfig) {
        let stages: [(Stage, PipelineStage); 2] = [
            (
                Stage::Coding {
                    claim: None,
                    plan: Default::default(),
                    retries: 0,
                },
                PipelineStage::Coder,
            ),
            (Stage::Qa, PipelineStage::Qa),
        ];

        for (pipeline_stage, stage) in &stages {
            let stage_dir = pipeline_stage.dir_name();
            let items = scan_stage_items(pipeline_stage);
            if items.is_empty() {
                continue;
            }

            for story_id in &items {
                // Items marked with review_hold (e.g. spikes after QA passes) stay
                // in their current stage for human review — don't auto-assign agents.
                if has_review_hold(story_id) {
                    continue;
                }

                // Skip frozen stories — pipeline advancement is suspended.
                if is_story_frozen(story_id) {
                    slog!("[auto-assign] Story '{story_id}' is frozen; skipping until unfrozen.");
                    continue;
                }

                // Skip blocked stories (retry limit exceeded).
                if is_story_blocked(story_id) {
                    continue;
                }

                // Skip stories whose dependencies haven't landed yet.
                if has_unmet_dependencies(story_id) {
                    slog!(
                        "[auto-assign] Story '{story_id}' has unmet dependencies; skipping until deps are done."
                    );
                    continue;
                }

                // Re-acquire the lock on each iteration to see state changes
                // from previous start_agent calls in the same pass.
                let preferred_agent = read_story_front_matter_agent(story_id);

                // Check max_coders limit for the Coder stage before agent selection.
                // If the pool is full, all remaining items in this stage wait.
                if *stage == PipelineStage::Coder
                    && let Some(max) = config.max_coders
                {
                    let agents_lock = self.agents.lock().await;
                    let active = count_active_agents_for_stage(config, &agents_lock, stage);
                    if active >= max {
                        slog!(
                            "[auto-assign] Coder pool full ({active}/{max}); remaining items in {stage_dir}/ will wait."
                        );
                        break;
                    }
                }

                // Outcome: (already_assigned, chosen_agent, preferred_busy, stage_mismatch)
                // preferred_busy=true means the story has a specific agent requested but it is
                // currently occupied — the story should wait rather than fall back.
                // stage_mismatch=true means the preferred agent's stage doesn't match the
                // pipeline stage, so we fell back to a generic stage agent.
                let (already_assigned, free_agent, preferred_busy, stage_mismatch) = {
                    let agents = self.agents.lock().await;
                    let assigned = is_story_assigned_for_stage(config, &agents, story_id, stage);
                    if assigned {
                        (true, None, false, false)
                    } else if let Some(ref pref) = preferred_agent {
                        // Story has a front-matter agent preference.
                        // Verify the preferred agent's stage matches the current
                        // pipeline stage — a coder shouldn't be assigned to QA.
                        let pref_stage_matches = config
                            .find_agent(pref)
                            .map(|cfg| super::super::super::agent_config_stage(cfg) == *stage)
                            .unwrap_or(false);
                        if !pref_stage_matches {
                            // Stage mismatch — fall back to any free agent for this stage.
                            let free = find_free_agent_for_stage(config, &agents, stage)
                                .map(|s| s.to_string());
                            (false, free, false, true)
                        } else if is_agent_free(&agents, pref) {
                            (false, Some(pref.clone()), false, false)
                        } else {
                            (false, None, true, false)
                        }
                    } else {
                        let free = find_free_agent_for_stage(config, &agents, stage)
                            .map(|s| s.to_string());
                        (false, free, false, false)
                    }
                };

                if already_assigned {
                    // Story already has an active agent — skip silently.
                    continue;
                }

                if preferred_busy {
                    // The story requests a specific agent that is currently busy.
                    // Do not fall back to a different agent; let this story wait.
                    slog!(
                        "[auto-assign] Preferred agent '{}' busy for '{story_id}'; story will wait.",
                        preferred_agent.as_deref().unwrap_or("?")
                    );
                    continue;
                }

                if stage_mismatch {
                    slog!(
                        "[auto-assign] Preferred agent '{}' stage mismatch for '{story_id}' in {stage_dir}/; falling back to stage-appropriate agent.",
                        preferred_agent.as_deref().unwrap_or("?")
                    );
                }

                match free_agent {
                    Some(agent_name) => {
                        slog!(
                            "[auto-assign] Assigning '{agent_name}' to '{story_id}' in {stage_dir}/"
                        );
                        if let Err(e) = self
                            .start_agent(project_root, story_id, Some(&agent_name), None, None)
                            .await
                        {
                            slog!(
                                "[auto-assign] Failed to start '{agent_name}' for '{story_id}': {e}"
                            );
                        }
                    }
                    None => {
                        // No free agents of this type — stop scanning this stage.
                        slog!(
                            "[auto-assign] All {:?} agents busy; remaining items in {stage_dir}/ will wait.",
                            stage
                        );
                        break;
                    }
                }
            }
        }
    }
}
