//! Use cases for ephemeral AI ticket editing assistants.

use std::sync::Arc;

use domain::ports::{
    AgentSessionFactory, SessionPlan, StructuredProviderLaunchPolicy,
    StructuredSessionEnvironmentPreparer, SystemPermissionStore,
};
use domain::profile::StructuredAdapter;
use domain::AgentProfile;
use domain::{
    AgentToolPolicy, AgentToolPolicyStore, AssistantContextProvider, DomainEvent,
    EffectivePermissions, EventBus, IssueRef, IssueStore, NetworkPolicy, PermissionStore,
    ProfileId, ProfileStore, Project, SessionId,
};

use crate::terminal::StructuredSessions;
use crate::AppError;

/// Opens an ephemeral ticket assistant chat session.
pub struct OpenTicketAssistant {
    issues: Arc<dyn IssueStore>,
    profiles: Arc<dyn ProfileStore>,
    contexts: Arc<dyn AssistantContextProvider>,
    environment: Arc<dyn StructuredSessionEnvironmentPreparer>,
    factory: Arc<dyn AgentSessionFactory>,
    structured: Arc<StructuredSessions>,
    policies: Arc<dyn AgentToolPolicyStore>,
    events: Arc<dyn EventBus>,
    permissions: Option<Arc<dyn PermissionStore>>,
    system_permissions: Option<Arc<dyn SystemPermissionStore>>,
}

/// Input for [`OpenTicketAssistant`].
pub struct OpenTicketAssistantInput {
    /// Project owning the ticket.
    pub project: Project,
    /// Ticket reference to bind.
    pub issue_ref: IssueRef,
    /// Structured profile to use.
    pub profile_id: ProfileId,
}

/// Output of [`OpenTicketAssistant`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenTicketAssistantOutput {
    /// Live structured session id, to be driven by `agent_send`.
    pub session_id: SessionId,
    /// MCP requester identity bound to this assistant.
    pub requester: String,
    /// Bound ticket reference.
    pub issue_ref: IssueRef,
}

impl OpenTicketAssistant {
    /// Builds the use case.
    #[allow(clippy::too_many_arguments)]
    #[must_use]
    pub fn new(
        issues: Arc<dyn IssueStore>,
        profiles: Arc<dyn ProfileStore>,
        contexts: Arc<dyn AssistantContextProvider>,
        environment: Arc<dyn StructuredSessionEnvironmentPreparer>,
        factory: Arc<dyn AgentSessionFactory>,
        structured: Arc<StructuredSessions>,
        policies: Arc<dyn AgentToolPolicyStore>,
        events: Arc<dyn EventBus>,
    ) -> Self {
        Self {
            issues,
            profiles,
            contexts,
            environment,
            factory,
            structured,
            policies,
            events,
            permissions: None,
            system_permissions: None,
        }
    }

    /// Wires the project agent permission store used for assistant launches.
    ///
    /// Ticket assistants are not agents, so only project defaults are resolved;
    /// agent overrides are deliberately ignored.
    #[must_use]
    pub fn with_permission_store(mut self, store: Arc<dyn PermissionStore>) -> Self {
        self.permissions = Some(store);
        self
    }

    /// Wires the project system permission store used for assistant launches.
    #[must_use]
    pub fn with_system_permission_store(mut self, store: Arc<dyn SystemPermissionStore>) -> Self {
        self.system_permissions = Some(store);
        self
    }

    /// Executes the open flow.
    ///
    /// Reopening an already-live assistant for the same ticket replaces the old
    /// session after the fresh one has started. This guarantees the assistant sees
    /// the latest ticket content/profile and keeps the invariant "one assistant per
    /// ticket" without leaving a gap if the new launch fails.
    pub async fn execute(
        &self,
        input: OpenTicketAssistantInput,
    ) -> Result<OpenTicketAssistantOutput, AppError> {
        let issue = self
            .issues
            .get_by_ref(&input.project.root, input.issue_ref)
            .await?;
        let profile = find_profile(&self.profiles, input.profile_id).await?;
        if !self.factory.supports(&profile) {
            return Err(AppError::Invalid(format!(
                "profile {} cannot start a structured ticket assistant",
                input.profile_id
            )));
        }
        let prepared = self
            .contexts
            .prepare_ticket_assistant_context(&input.project, &issue)
            .await
            .map_err(|e| AppError::Store(e.to_string()))?;
        let requester = ticket_assistant_requester(&input.project, input.issue_ref);
        let had_existing = self
            .structured
            .ticket_assistant_requester(input.issue_ref)
            .is_some();
        let permissions = self
            .resolve_project_default_permissions(&input.project)
            .await?;
        let policy = AgentToolPolicy::new(
            vec![
                "idea_ticket_read".to_owned(),
                "idea_ticket_update".to_owned(),
                "idea_ticket_update_status".to_owned(),
                "idea_ticket_update_priority".to_owned(),
                "idea_ticket_read_carnet".to_owned(),
                "idea_ticket_update_carnet".to_owned(),
                "idea_ticket_attachment_read".to_owned(),
                "idea_ticket_attachment_mark_summarized".to_owned(),
                "idea_ticket_link".to_owned(),
                "idea_ticket_unlink".to_owned(),
            ],
            Some(input.issue_ref),
            true,
        );
        self.policies.set_policy(requester.clone(), policy);
        let environment = match self
            .environment
            .prepare_ticket_assistant(
                &input.project,
                input.issue_ref,
                &profile,
                &prepared,
                &requester,
                permissions.as_ref(),
            )
            .await
        {
            Ok(environment) => environment,
            Err(err) => {
                if !had_existing {
                    self.policies.clear_policy(&requester);
                }
                return Err(AppError::from(err));
            }
        };
        let structured_policy = self
            .build_ticket_assistant_structured_policy(&input.project, &profile, &environment)
            .await?;
        let session = self
            .factory
            .start(
                &profile,
                &prepared,
                &environment.cwd,
                &SessionPlan::None,
                Some(&requester),
                &environment.env,
                None,
                structured_policy
                    .as_ref()
                    .or(environment.structured_policy.as_ref()),
            )
            .await
            .map_err(|err| {
                if !had_existing {
                    self.policies.clear_policy(&requester);
                }
                AppError::from(err)
            })?;
        let session_id = session.id();

        if let Some(old_requester) = self.structured.ticket_assistant_requester(input.issue_ref) {
            if old_requester != requester {
                self.policies.clear_policy(&old_requester);
            }
        }
        let replaced = self.structured.insert_ticket_assistant(
            input.issue_ref,
            requester.clone(),
            Arc::clone(&session),
        );
        if let Some(old) = replaced {
            let _ = old.shutdown().await;
        }
        self.events.publish(DomainEvent::TicketAssistantOpened {
            issue_ref: input.issue_ref,
            profile_id: input.profile_id,
        });

        Ok(OpenTicketAssistantOutput {
            session_id,
            requester,
            issue_ref: input.issue_ref,
        })
    }

    async fn build_ticket_assistant_structured_policy(
        &self,
        project: &Project,
        profile: &AgentProfile,
        environment: &domain::ports::StructuredSessionEnvironment,
    ) -> Result<Option<StructuredProviderLaunchPolicy>, AppError> {
        if profile.structured_adapter != Some(StructuredAdapter::Codex) {
            return Ok(environment.structured_policy.clone());
        }
        let network = self.resolve_project_default_network(project).await?;
        Ok(Some(StructuredProviderLaunchPolicy::Codex {
            sandbox_mode: "workspace-write".to_owned(),
            writable_roots: vec![project.root.as_str().to_owned()],
            network_access: codex_network_access(network),
        }))
    }

    async fn resolve_project_default_network(
        &self,
        project: &Project,
    ) -> Result<Option<NetworkPolicy>, AppError> {
        let Some(store) = &self.system_permissions else {
            return Ok(None);
        };
        let doc = store.load_system_permissions(project).await?;
        Ok(doc.project_default.and_then(|set| set.network))
    }

    async fn resolve_project_default_permissions(
        &self,
        project: &Project,
    ) -> Result<Option<EffectivePermissions>, AppError> {
        let Some(store) = &self.permissions else {
            return Ok(None);
        };
        let doc = store.load_permissions(project).await?;
        Ok(domain::permission::resolve(
            doc.project_defaults.as_ref(),
            None,
        ))
    }
}

fn codex_network_access(network: Option<NetworkPolicy>) -> bool {
    matches!(network, Some(NetworkPolicy::Allow))
}

fn ticket_assistant_requester(project: &Project, issue_ref: IssueRef) -> String {
    format!(
        "ticket-assistant:{}:{}",
        project.id.as_uuid().simple(),
        issue_ref.number().get()
    )
}

/// Closes an ephemeral ticket assistant chat session.
pub struct CloseTicketAssistant {
    structured: Arc<StructuredSessions>,
    policies: Arc<dyn AgentToolPolicyStore>,
    events: Arc<dyn EventBus>,
}

/// Input for [`CloseTicketAssistant`].
pub struct CloseTicketAssistantInput {
    /// Ticket reference whose assistant should be closed.
    pub issue_ref: IssueRef,
}

impl CloseTicketAssistant {
    /// Builds the use case.
    #[must_use]
    pub fn new(
        structured: Arc<StructuredSessions>,
        policies: Arc<dyn AgentToolPolicyStore>,
        events: Arc<dyn EventBus>,
    ) -> Self {
        Self {
            structured,
            policies,
            events,
        }
    }

    /// Executes the close flow.
    pub async fn execute(&self, input: CloseTicketAssistantInput) -> Result<(), AppError> {
        let (session, requester) = self
            .structured
            .remove_ticket_assistant(input.issue_ref)
            .ok_or_else(|| AppError::NotFound(format!("ticket assistant {}", input.issue_ref)))?;
        self.policies.clear_policy(&requester);
        session.shutdown().await.map_err(AppError::from)?;
        self.events.publish(DomainEvent::TicketAssistantClosed {
            issue_ref: input.issue_ref,
        });
        Ok(())
    }
}

async fn find_profile(
    profiles: &Arc<dyn ProfileStore>,
    profile_id: ProfileId,
) -> Result<AgentProfile, AppError> {
    profiles
        .list()
        .await?
        .into_iter()
        .find(|profile| profile.id == profile_id)
        .ok_or_else(|| AppError::NotFound(format!("profile {profile_id}")))
}
