//! Profile use cases (ARCHITECTURE §6, L5). Each is a single-responsibility
//! struct carrying its ports as `Arc<dyn Port>` and exposing one `execute`.
//!
//! - [`DetectProfiles`] — probe a set of candidate profiles via [`AgentRuntime`]
//!   and report which CLIs are installed (first-run availability ✓/✗).
//! - [`ListProfiles`] / [`SaveProfile`] / [`DeleteProfile`] — CRUD over the
//!   persisted profiles through the [`ProfileStore`].
//! - [`ConfigureProfiles`] — persist a batch of chosen/edited/custom profiles
//!   (closes the first-run wizard).
//! - [`ReferenceProfiles`] — expose the pre-filled, editable catalogue.
//! - [`FirstRunState`] — tell the UI whether the first-run wizard should show
//!   (no `profiles.json` yet) and hand it the reference catalogue.

use std::sync::Arc;

use domain::ids::ProfileId;
use domain::ports::{AgentRuntime, IdGenerator, ProfileStore};
use domain::profile::{AgentProfile, OpenCodeConfig, StructuredAdapter};

use crate::error::AppError;

use super::catalogue::{reference_profile_id, reference_profiles, selectable_reference_profiles};

// ---------------------------------------------------------------------------
// DetectProfiles
// ---------------------------------------------------------------------------

/// Input for [`DetectProfiles::execute`]: the candidate profiles to probe.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectProfilesInput {
    /// Profiles whose `detect` command should be run.
    pub candidates: Vec<AgentProfile>,
}

/// Availability of a single candidate after detection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileAvailability {
    /// The probed profile.
    pub profile: AgentProfile,
    /// Whether its CLI was detected as installed (exit code 0).
    pub available: bool,
}

/// Output of [`DetectProfiles::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectProfilesOutput {
    /// One entry per candidate (same order), with its availability.
    pub results: Vec<ProfileAvailability>,
}

/// Probes candidate profiles' detection commands and reports availability.
pub struct DetectProfiles {
    runtime: Arc<dyn AgentRuntime>,
}

impl DetectProfiles {
    /// Builds the use case from the [`AgentRuntime`] port. The runtime itself
    /// holds the [`domain::ports::ProcessSpawner`] used for detection.
    #[must_use]
    pub fn new(runtime: Arc<dyn AgentRuntime>) -> Self {
        Self { runtime }
    }

    /// Runs detection for each candidate. A detection *error* (e.g. the command
    /// could not even be launched) is reported as `available: false`, not a
    /// hard failure — the wizard just shows ✗ and the user can still keep the
    /// profile.
    ///
    /// # Errors
    /// Currently never returns `Err` (failures degrade to `available: false`);
    /// the `Result` keeps the signature uniform with the other use cases.
    pub async fn execute(
        &self,
        input: DetectProfilesInput,
    ) -> Result<DetectProfilesOutput, AppError> {
        let mut results = Vec::with_capacity(input.candidates.len());
        let mut probes = Vec::with_capacity(input.candidates.len());
        for profile in input.candidates {
            let runtime = Arc::clone(&self.runtime);
            probes.push(tokio::spawn(async move {
                let available = runtime.detect(&profile).await.unwrap_or(false);
                ProfileAvailability { profile, available }
            }));
        }
        for probe in probes {
            results.push(probe.await.map_err(|err| {
                AppError::Process(format!("profile detection task failed: {err}"))
            })?);
        }
        Ok(DetectProfilesOutput { results })
    }
}

// ---------------------------------------------------------------------------
// ListProfiles
// ---------------------------------------------------------------------------

/// Output of [`ListProfiles::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListProfilesOutput {
    /// All configured profiles.
    pub profiles: Vec<AgentProfile>,
}

/// Lists the configured profiles from the store.
pub struct ListProfiles {
    store: Arc<dyn ProfileStore>,
}

impl ListProfiles {
    /// Builds the use case from the [`ProfileStore`] port.
    #[must_use]
    pub fn new(store: Arc<dyn ProfileStore>) -> Self {
        Self { store }
    }

    /// Lists configured profiles.
    ///
    /// # Errors
    /// [`AppError::Store`] on persistence failure.
    pub async fn execute(&self) -> Result<ListProfilesOutput, AppError> {
        Ok(ListProfilesOutput {
            profiles: self.store.list().await?,
        })
    }
}

// ---------------------------------------------------------------------------
// SaveProfile
// ---------------------------------------------------------------------------

/// Input for [`SaveProfile::execute`]: the profile to upsert.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SaveProfileInput {
    /// The profile to create or replace (by id).
    pub profile: AgentProfile,
}

/// Output of [`SaveProfile::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SaveProfileOutput {
    /// The saved profile (echoed back).
    pub profile: AgentProfile,
}

// ---------------------------------------------------------------------------
// CloneOpenCodeProfileFromSeed
// ---------------------------------------------------------------------------

/// Input for [`CloneOpenCodeProfileFromSeed::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloneOpenCodeProfileFromSeedInput {
    /// Optional display name for the cloned profile. When absent, a copy label is
    /// derived from the seed name.
    pub name: Option<String>,
    /// Optional OpenCode config override. When absent, the seed config is copied.
    pub opencode: Option<OpenCodeConfig>,
}

/// Output of [`CloneOpenCodeProfileFromSeed::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloneOpenCodeProfileFromSeedOutput {
    /// The newly persisted profile.
    pub profile: AgentProfile,
}

/// Creates a new OpenCode profile instance from the canonical seed/template.
///
/// The persisted canonical `opencode-llamacpp` profile is preferred when present,
/// so local edits are preserved as the clone template. If it is absent, the
/// in-memory reference catalogue seed is used. The new profile always receives a
/// fresh [`ProfileId`], which is the only identity constraint; multiple profiles
/// with `StructuredAdapter::OpenCode` are therefore valid.
pub struct CloneOpenCodeProfileFromSeed {
    store: Arc<dyn ProfileStore>,
    ids: Arc<dyn IdGenerator>,
}

impl CloneOpenCodeProfileFromSeed {
    /// Builds the use case from the profile store and id generator ports.
    #[must_use]
    pub fn new(store: Arc<dyn ProfileStore>, ids: Arc<dyn IdGenerator>) -> Self {
        Self { store, ids }
    }

    /// Clones the canonical OpenCode seed into a new persisted profile.
    ///
    /// # Errors
    /// [`AppError::Store`] on persistence failure, [`AppError::Invalid`] if the
    /// requested name is blank, or [`AppError::Internal`] if the seed is malformed.
    pub async fn execute(
        &self,
        input: CloneOpenCodeProfileFromSeedInput,
    ) -> Result<CloneOpenCodeProfileFromSeedOutput, AppError> {
        let existing = self.store.list().await?;
        let seed_id = reference_profile_id("opencode-llamacpp");
        let seed = existing
            .iter()
            .find(|profile| profile.id == seed_id)
            .cloned()
            .or_else(|| {
                reference_profiles()
                    .into_iter()
                    .find(|profile| profile.id == seed_id)
            })
            .ok_or_else(|| {
                AppError::Internal("canonical OpenCode seed `opencode-llamacpp` is missing".into())
            })?;

        if seed.structured_adapter != Some(StructuredAdapter::OpenCode) || seed.opencode.is_none() {
            return Err(AppError::Internal(
                "canonical OpenCode seed is not an OpenCode profile".into(),
            ));
        }

        let mut profile = seed;
        profile.id = fresh_profile_id(&*self.ids, &existing)?;
        profile.name = match input.name {
            Some(name) => {
                if name.trim().is_empty() {
                    return Err(AppError::Invalid("profile.name must not be empty".into()));
                }
                name
            }
            None => format!("{} copy", profile.name),
        };
        if let Some(config) = input.opencode {
            profile.opencode = Some(config);
        }

        self.store.save(&profile).await?;
        Ok(CloneOpenCodeProfileFromSeedOutput { profile })
    }
}

fn fresh_profile_id(
    ids: &dyn IdGenerator,
    existing: &[AgentProfile],
) -> Result<ProfileId, AppError> {
    for _ in 0..16 {
        let id = ProfileId::from_uuid(ids.new_uuid());
        if existing.iter().all(|profile| profile.id != id) {
            return Ok(id);
        }
    }
    Err(AppError::Internal(
        "could not allocate a unique profile id".into(),
    ))
}

/// Persists (creates or replaces) a single profile.
pub struct SaveProfile {
    store: Arc<dyn ProfileStore>,
}

impl SaveProfile {
    /// Builds the use case from the [`ProfileStore`] port.
    #[must_use]
    pub fn new(store: Arc<dyn ProfileStore>) -> Self {
        Self { store }
    }

    /// Saves the profile.
    ///
    /// # Errors
    /// [`AppError::Store`] on persistence failure.
    pub async fn execute(&self, input: SaveProfileInput) -> Result<SaveProfileOutput, AppError> {
        self.store.save(&input.profile).await?;
        Ok(SaveProfileOutput {
            profile: input.profile,
        })
    }
}

// ---------------------------------------------------------------------------
// DeleteProfile
// ---------------------------------------------------------------------------

/// Input for [`DeleteProfile::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteProfileInput {
    /// Id of the profile to delete.
    pub id: domain::ids::ProfileId,
}

/// Deletes a profile by id.
pub struct DeleteProfile {
    store: Arc<dyn ProfileStore>,
}

impl DeleteProfile {
    /// Builds the use case from the [`ProfileStore`] port.
    #[must_use]
    pub fn new(store: Arc<dyn ProfileStore>) -> Self {
        Self { store }
    }

    /// Deletes the profile.
    ///
    /// # Errors
    /// [`AppError::NotFound`] if the id is unknown, [`AppError::Store`] on
    /// persistence failure.
    pub async fn execute(&self, input: DeleteProfileInput) -> Result<(), AppError> {
        self.store.delete(input.id).await?;
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// ConfigureProfiles
// ---------------------------------------------------------------------------

/// Input for [`ConfigureProfiles::execute`]: the chosen/edited/custom profiles.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigureProfilesInput {
    /// All profiles the user decided to keep (closes the first run).
    pub profiles: Vec<AgentProfile>,
}

/// Output of [`ConfigureProfiles::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigureProfilesOutput {
    /// The persisted profiles.
    pub profiles: Vec<AgentProfile>,
}

/// Persists the batch of profiles chosen at the end of the first-run wizard.
///
/// Saving even an empty list creates `profiles.json`, which marks the first run
/// as done (so the wizard does not reappear).
pub struct ConfigureProfiles {
    store: Arc<dyn ProfileStore>,
}

impl ConfigureProfiles {
    /// Builds the use case from the [`ProfileStore`] port.
    #[must_use]
    pub fn new(store: Arc<dyn ProfileStore>) -> Self {
        Self { store }
    }

    /// Persists each chosen profile.
    ///
    /// # Errors
    /// [`AppError::Store`] on persistence failure.
    pub async fn execute(
        &self,
        input: ConfigureProfilesInput,
    ) -> Result<ConfigureProfilesOutput, AppError> {
        for profile in &input.profiles {
            self.store.save(profile).await?;
        }
        // Ensure `profiles.json` exists even when the user kept nothing, so the
        // first run is recorded as complete.
        if input.profiles.is_empty() {
            self.store.mark_configured().await?;
        }
        Ok(ConfigureProfilesOutput {
            profiles: input.profiles,
        })
    }
}

// ---------------------------------------------------------------------------
// ReferenceProfiles (catalogue accessor)
// ---------------------------------------------------------------------------

/// Output of [`ReferenceProfiles::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReferenceProfilesOutput {
    /// The pre-filled, editable reference catalogue, **restricted to the
    /// selectable profiles** (§17.3, D7): only profiles drivable in structured
    /// mode are offered to selection/creation. Today: Claude + Codex.
    pub profiles: Vec<AgentProfile>,
}

/// Exposes the **selectable** reference catalogue for the agent-creation menu
/// (§17.3, D7): the structured-drivable profiles only (Claude/Codex). Gemini and
/// Aider remain in the raw catalogue data but are not proposed here.
#[derive(Default)]
pub struct ReferenceProfiles;

impl ReferenceProfiles {
    /// Builds the (stateless) use case.
    #[must_use]
    pub fn new() -> Self {
        Self
    }

    /// Returns the reference catalogue. Infallible.
    ///
    /// # Errors
    /// Never; the `Result` keeps the call site uniform.
    #[allow(clippy::unused_async)]
    pub async fn execute(&self) -> Result<ReferenceProfilesOutput, AppError> {
        Ok(ReferenceProfilesOutput {
            profiles: selectable_reference_profiles(),
        })
    }
}

// ---------------------------------------------------------------------------
// FirstRunState
// ---------------------------------------------------------------------------

/// Output of [`FirstRunState::execute`]: whether to show the wizard + catalogue.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FirstRunStateOutput {
    /// `true` when no `profiles.json` exists yet ⇒ show the first-run wizard.
    pub is_first_run: bool,
    /// The pre-filled reference catalogue to seed the wizard, **restricted to the
    /// selectable profiles** (§17.3, D7): only structured-drivable profiles
    /// (Claude/Codex) are offered. No custom-profile entry.
    pub reference_profiles: Vec<AgentProfile>,
}

/// Reports whether the IDE is on its first run (no profiles configured yet) and
/// provides the reference catalogue to seed the wizard.
pub struct FirstRunState {
    store: Arc<dyn ProfileStore>,
}

impl FirstRunState {
    /// Builds the use case from the [`ProfileStore`] port.
    #[must_use]
    pub fn new(store: Arc<dyn ProfileStore>) -> Self {
        Self { store }
    }

    /// Computes the first-run state.
    ///
    /// # Errors
    /// [`AppError::Store`] on persistence failure.
    pub async fn execute(&self) -> Result<FirstRunStateOutput, AppError> {
        let configured = self.store.is_configured().await?;
        Ok(FirstRunStateOutput {
            is_first_run: !configured,
            reference_profiles: selectable_reference_profiles(),
        })
    }
}
