//! Hook events are append-only across requirements layers. The managed hook
//! directory is different: only one directory is usable on a given platform, so
//! conflicting values for the active platform fail closed. The inactive platform
//! field is first-filled to allow the same layer stack to carry OS-specific
//! directories.

use crate::HookEventsToml;
use crate::ManagedHooksRequirementsToml;
use crate::RequirementSource;
use crate::Sourced;
use std::collections::BTreeMap;
use std::path::PathBuf;

use super::stack::composition_conflict;
use super::stack::merge_output_source;

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum HookDirectoryField {
    #[default]
    ManagedDir,
    WindowsManagedDir,
}

impl HookDirectoryField {
    pub(super) fn current_platform() -> Self {
        if cfg!(windows) {
            Self::WindowsManagedDir
        } else {
            Self::ManagedDir
        }
    }

    fn field_name(self) -> &'static str {
        match self {
            Self::ManagedDir => "hooks.managed_dir",
            Self::WindowsManagedDir => "hooks.windows_managed_dir",
        }
    }

    fn inactive(self) -> Self {
        match self {
            Self::ManagedDir => Self::WindowsManagedDir,
            Self::WindowsManagedDir => Self::ManagedDir,
        }
    }
}

pub(super) struct HookMergeState {
    directory_field: HookDirectoryField,
    dir_sources: BTreeMap<HookDirectoryField, RequirementSource>,
}

impl HookMergeState {
    pub(super) fn new(directory_field: HookDirectoryField) -> Self {
        Self {
            directory_field,
            dir_sources: BTreeMap::new(),
        }
    }

    pub(super) fn merge(
        &mut self,
        target: &mut Option<Sourced<ManagedHooksRequirementsToml>>,
        incoming: Option<ManagedHooksRequirementsToml>,
        source: &RequirementSource,
    ) -> Result<(), super::stack::RequirementsCompositionError> {
        let Some(mut incoming) = incoming.filter(|value| !value.is_empty()) else {
            return Ok(());
        };
        let Some(existing) = target.as_mut() else {
            self.track_singleton_source(
                HookDirectoryField::ManagedDir,
                &incoming.managed_dir,
                source,
            );
            self.track_singleton_source(
                HookDirectoryField::WindowsManagedDir,
                &incoming.windows_managed_dir,
                source,
            );
            *target = Some(Sourced::new(incoming, source.clone()));
            return Ok(());
        };

        let active_field = self.directory_field;
        let inactive_field = active_field.inactive();
        let incoming_active_dir = take_hook_dir(&mut incoming, active_field);
        let incoming_inactive_dir = take_hook_dir(&mut incoming, inactive_field);
        let mut changed = false;
        changed |= self.merge_active_singleton(
            active_field,
            hook_dir_mut(&mut existing.value, active_field),
            incoming_active_dir,
            source,
        )?;
        changed |= self.fill_singleton(
            inactive_field,
            hook_dir_mut(&mut existing.value, inactive_field),
            incoming_inactive_dir,
            source,
        );
        changed |= append_hook_events(&mut existing.value.hooks, incoming.hooks);
        if changed {
            merge_output_source(&mut existing.source, source);
        }
        Ok(())
    }

    fn track_singleton_source(
        &mut self,
        field: HookDirectoryField,
        value: &Option<PathBuf>,
        source: &RequirementSource,
    ) {
        if value.is_some() {
            self.dir_sources
                .entry(field)
                .or_insert_with(|| source.clone());
        }
    }

    fn merge_active_singleton(
        &mut self,
        field: HookDirectoryField,
        existing: &mut Option<PathBuf>,
        incoming: Option<PathBuf>,
        incoming_source: &RequirementSource,
    ) -> Result<bool, super::stack::RequirementsCompositionError> {
        let Some(incoming) = incoming else {
            return Ok(false);
        };

        match existing {
            Some(existing_value) if existing_value != &incoming => {
                let existing_source = self
                    .dir_sources
                    .get(&field)
                    .cloned()
                    .unwrap_or_else(|| incoming_source.clone());
                Err(composition_conflict(
                    field.field_name().to_string(),
                    existing_source,
                    incoming_source.clone(),
                    format!(
                        "`{}` conflicts with `{}`",
                        existing_value.display(),
                        incoming.display()
                    ),
                ))
            }
            Some(_) => Ok(false),
            None => {
                *existing = Some(incoming);
                self.dir_sources
                    .entry(field)
                    .or_insert_with(|| incoming_source.clone());
                Ok(true)
            }
        }
    }

    fn fill_singleton(
        &mut self,
        field: HookDirectoryField,
        existing: &mut Option<PathBuf>,
        incoming: Option<PathBuf>,
        incoming_source: &RequirementSource,
    ) -> bool {
        if existing.is_none()
            && let Some(incoming) = incoming
        {
            *existing = Some(incoming);
            self.dir_sources
                .entry(field)
                .or_insert_with(|| incoming_source.clone());
            true
        } else {
            false
        }
    }
}

fn take_hook_dir(
    hooks: &mut ManagedHooksRequirementsToml,
    field: HookDirectoryField,
) -> Option<PathBuf> {
    match field {
        HookDirectoryField::ManagedDir => hooks.managed_dir.take(),
        HookDirectoryField::WindowsManagedDir => hooks.windows_managed_dir.take(),
    }
}

fn hook_dir_mut(
    hooks: &mut ManagedHooksRequirementsToml,
    field: HookDirectoryField,
) -> &mut Option<PathBuf> {
    match field {
        HookDirectoryField::ManagedDir => &mut hooks.managed_dir,
        HookDirectoryField::WindowsManagedDir => &mut hooks.windows_managed_dir,
    }
}

fn append_hook_events(existing: &mut HookEventsToml, incoming: HookEventsToml) -> bool {
    // Destructure without `..` so new hook events cannot be introduced without
    // deciding whether requirements layer merging should append them.
    let HookEventsToml {
        pre_tool_use,
        permission_request,
        post_tool_use,
        pre_compact,
        post_compact,
        session_start,
        user_prompt_submit,
        subagent_start,
        subagent_stop,
        stop,
    } = incoming;

    let mut changed = false;
    changed |= append_vec(&mut existing.pre_tool_use, pre_tool_use);
    changed |= append_vec(&mut existing.permission_request, permission_request);
    changed |= append_vec(&mut existing.post_tool_use, post_tool_use);
    changed |= append_vec(&mut existing.pre_compact, pre_compact);
    changed |= append_vec(&mut existing.post_compact, post_compact);
    changed |= append_vec(&mut existing.session_start, session_start);
    changed |= append_vec(&mut existing.user_prompt_submit, user_prompt_submit);
    changed |= append_vec(&mut existing.subagent_start, subagent_start);
    changed |= append_vec(&mut existing.subagent_stop, subagent_stop);
    changed |= append_vec(&mut existing.stop, stop);
    changed
}

fn append_vec<T>(existing: &mut Vec<T>, mut incoming: Vec<T>) -> bool {
    let changed = !incoming.is_empty();
    existing.append(&mut incoming);
    changed
}
