//! Chat application facade: HTTP command handling, SQLx persistence, and outbox publication.
//!
//! Durable writes are exposed via [`ChatApp`] and map to [`openapi.yaml`](../../openapi.yaml)
//! `/chat/*` routes; committed events are relayed to WebSocket clients through
//! [`ChatEventPublisher`].

use std::collections::HashSet;
use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use sqlx::PgPool;
use tracing::instrument;

pub mod authz;
pub mod domain;
pub mod dto;
pub mod error;
pub mod events;
pub mod ids;
pub mod outbox;
pub mod repo;
pub mod schema;
pub mod service;

use dto::commands::{
    AddMembers, AddReaction, ArchiveRoom, CreateRoom, DeleteMessage, EditMessage, ListMessages,
    ListRooms, MarkReadUpTo, RemoveMember, RemoveReaction, SearchMessages, SendMessage, UpdateRoom,
};
use dto::results::{
    DeleteResult, MemberView, MessagePage, MessageView, ReactionSummary, ReadCursorView,
    RemoveResult, RoomPage, RoomView, SearchPage,
};
pub use error::{ChatError, ChatResult};
use ids::{MessageId, OrganizationId, RoomId, UserId};
pub use outbox::{ChatEventPublisher, NoopChatEventPublisher};
use repo::sqlx::SqlxChatRepository;
use repo::traits::ChatRepository;
use service::member_service::MemberService;
use service::message_service::MessageService;
use service::receipt_service::ReceiptService;
use service::room_service::RoomService;
use service::search_service::SearchService;

/// Per-request actor and tenant scope resolved from Athena Auth session.
#[derive(Debug, Clone)]
pub struct ChatContext {
    pub user_id: UserId,
    pub organization_id: OrganizationId,
    pub client_name: String,
    pub is_admin: bool,
    pub role_ids: Vec<String>,
    pub request_id: Option<String>,
}

/// Resolves tenant-scoped PostgreSQL pools for chat data and outbox workers.
#[async_trait]
pub trait ChatPoolResolver: Send + Sync {
    async fn resolve_pool(&self, client_name: &str) -> ChatResult<PgPool>;
    async fn list_pools(&self) -> ChatResult<Vec<(String, PgPool)>>;
}

/// Application boundary for all chat mutations and queries.
///
/// Implementations enforce authorization, persist state, enqueue outbox rows, and publish events.
#[async_trait]
pub trait ChatApp: Send + Sync {
    async fn create_room(&self, ctx: ChatContext, input: CreateRoom) -> ChatResult<RoomView>;
    async fn list_rooms(&self, ctx: ChatContext, input: ListRooms) -> ChatResult<RoomPage>;
    async fn get_room(&self, ctx: ChatContext, room_id: RoomId) -> ChatResult<RoomView>;
    async fn update_room(&self, ctx: ChatContext, input: UpdateRoom) -> ChatResult<RoomView>;
    async fn archive_room(&self, ctx: ChatContext, input: ArchiveRoom) -> ChatResult<RoomView>;
    async fn list_messages(&self, ctx: ChatContext, input: ListMessages)
    -> ChatResult<MessagePage>;
    async fn send_message(&self, ctx: ChatContext, input: SendMessage) -> ChatResult<MessageView>;
    async fn edit_message(&self, ctx: ChatContext, input: EditMessage) -> ChatResult<MessageView>;
    async fn delete_message(
        &self,
        ctx: ChatContext,
        input: DeleteMessage,
    ) -> ChatResult<DeleteResult>;
    async fn mark_read_up_to(
        &self,
        ctx: ChatContext,
        input: MarkReadUpTo,
    ) -> ChatResult<ReadCursorView>;
    async fn add_reaction(
        &self,
        ctx: ChatContext,
        input: AddReaction,
    ) -> ChatResult<ReactionSummary>;
    async fn remove_reaction(
        &self,
        ctx: ChatContext,
        input: RemoveReaction,
    ) -> ChatResult<ReactionSummary>;
    async fn list_members(&self, ctx: ChatContext, room_id: RoomId) -> ChatResult<Vec<MemberView>>;
    async fn add_members(&self, ctx: ChatContext, input: AddMembers)
    -> ChatResult<Vec<MemberView>>;
    async fn remove_member(
        &self,
        ctx: ChatContext,
        input: RemoveMember,
    ) -> ChatResult<RemoveResult>;
    async fn search_messages(
        &self,
        ctx: ChatContext,
        input: SearchMessages,
    ) -> ChatResult<SearchPage>;
    async fn publish_pending_outbox(&self, limit_per_pool: usize) -> ChatResult<u64>;
}

/// Production [`ChatApp`] backed by SQLx repositories and a pluggable event publisher.
pub struct SqlxChatApp {
    pools: Arc<dyn ChatPoolResolver>,
    publisher: Arc<dyn ChatEventPublisher>,
    rooms: RoomService,
    messages: MessageService,
    members: MemberService,
    receipts: ReceiptService,
    search: SearchService,
    repo: Arc<dyn ChatRepository>,
    schema_ready: Mutex<HashSet<String>>,
}

impl SqlxChatApp {
    /// Constructs a chat app with the given pool resolver and realtime publisher.
    pub fn new(pools: Arc<dyn ChatPoolResolver>, publisher: Arc<dyn ChatEventPublisher>) -> Self {
        let repo: Arc<dyn ChatRepository> = Arc::new(SqlxChatRepository::new());
        Self {
            pools,
            publisher,
            rooms: RoomService::new(repo.clone()),
            messages: MessageService::new(repo.clone()),
            members: MemberService::new(repo.clone()),
            receipts: ReceiptService::new(repo.clone()),
            search: SearchService::new(repo.clone()),
            repo,
            schema_ready: Mutex::new(HashSet::new()),
        }
    }

    async fn pool_for(&self, ctx: &ChatContext) -> ChatResult<PgPool> {
        let pool = self.pools.resolve_pool(&ctx.client_name).await?;
        schema::ensure_chat_runtime_schema(&pool, &ctx.client_name, &self.schema_ready).await?;
        Ok(pool)
    }
}

#[async_trait]
impl ChatApp for SqlxChatApp {
    #[instrument(name = "chat.create_room", skip(self, ctx, input), fields(user_id = %ctx.user_id, organization_id = %ctx.organization_id))]
    async fn create_room(&self, ctx: ChatContext, input: CreateRoom) -> ChatResult<RoomView> {
        let pool = self.pool_for(&ctx).await?;
        let commit = self
            .rooms
            .create_room(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.room.id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(commit.room)
    }

    #[instrument(name = "chat.list_rooms", skip(self, ctx, input), fields(user_id = %ctx.user_id, organization_id = %ctx.organization_id))]
    async fn list_rooms(&self, ctx: ChatContext, input: ListRooms) -> ChatResult<RoomPage> {
        let pool = self.pool_for(&ctx).await?;
        self.rooms
            .list_rooms(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await
    }

    #[instrument(name = "chat.get_room", skip(self, ctx), fields(user_id = %ctx.user_id, organization_id = %ctx.organization_id, room_id = %room_id))]
    async fn get_room(&self, ctx: ChatContext, room_id: RoomId) -> ChatResult<RoomView> {
        let pool = self.pool_for(&ctx).await?;
        self.rooms
            .get_room(&pool, &ctx.organization_id, &ctx.user_id, room_id)
            .await
    }

    async fn update_room(&self, ctx: ChatContext, input: UpdateRoom) -> ChatResult<RoomView> {
        let pool = self.pool_for(&ctx).await?;
        let commit = self
            .rooms
            .update_room(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.room.id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(commit.room)
    }

    async fn archive_room(&self, ctx: ChatContext, input: ArchiveRoom) -> ChatResult<RoomView> {
        let pool = self.pool_for(&ctx).await?;
        let commit = self
            .rooms
            .archive_room(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.room.id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(commit.room)
    }

    #[instrument(name = "chat.list_messages", skip(self, ctx, input), fields(user_id = %ctx.user_id, organization_id = %ctx.organization_id, room_id = %input.room_id))]
    async fn list_messages(
        &self,
        ctx: ChatContext,
        input: ListMessages,
    ) -> ChatResult<MessagePage> {
        let pool = self.pool_for(&ctx).await?;
        self.messages
            .list_messages(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await
    }

    #[instrument(name = "chat.send_message", skip(self, ctx, input), fields(user_id = %ctx.user_id, organization_id = %ctx.organization_id, room_id = %input.room_id))]
    async fn send_message(&self, ctx: ChatContext, input: SendMessage) -> ChatResult<MessageView> {
        let pool = self.pool_for(&ctx).await?;
        let commit = self
            .messages
            .send_message(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await?;
        if let Some(outbox_id) = commit.outbox_id {
            self.publisher
                .publish_room_event(&ctx.client_name, commit.message.room_id, commit.event)
                .await;
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(commit.message)
    }

    #[instrument(name = "chat.edit_message", skip(self, ctx, input), fields(user_id = %ctx.user_id, organization_id = %ctx.organization_id, room_id = %input.room_id, message_id = %input.message_id))]
    async fn edit_message(&self, ctx: ChatContext, input: EditMessage) -> ChatResult<MessageView> {
        let pool = self.pool_for(&ctx).await?;
        let commit = self
            .messages
            .edit_message(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.message.room_id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(commit.message)
    }

    #[instrument(name = "chat.delete_message", skip(self, ctx, input), fields(user_id = %ctx.user_id, organization_id = %ctx.organization_id, room_id = %input.room_id, message_id = %input.message_id))]
    async fn delete_message(
        &self,
        ctx: ChatContext,
        input: DeleteMessage,
    ) -> ChatResult<DeleteResult> {
        let pool = self.pool_for(&ctx).await?;
        let commit = self
            .messages
            .delete_message(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.room_id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(DeleteResult {
            ok: true,
            message_id: commit.message_id,
        })
    }

    #[instrument(name = "chat.mark_read_up_to", skip(self, ctx, input), fields(user_id = %ctx.user_id, organization_id = %ctx.organization_id, room_id = %input.room_id))]
    async fn mark_read_up_to(
        &self,
        ctx: ChatContext,
        input: MarkReadUpTo,
    ) -> ChatResult<ReadCursorView> {
        let pool = self.pool_for(&ctx).await?;
        let seq = input.seq.unwrap_or(0);
        let commit = self
            .receipts
            .mark_read_up_to(
                &pool,
                &ctx.organization_id,
                &ctx.user_id,
                input.room_id,
                seq,
                input.message_id,
            )
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.room_id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(ReadCursorView {
            room_id: commit.room_id,
            user_id: commit.user_id,
            last_read_seq: commit.last_read_seq,
            last_read_message_id: commit.last_read_message_id,
        })
    }

    async fn add_reaction(
        &self,
        ctx: ChatContext,
        input: AddReaction,
    ) -> ChatResult<ReactionSummary> {
        let pool = self.pool_for(&ctx).await?;
        let commit = self
            .repo
            .add_reaction(
                &pool,
                &ctx.organization_id,
                &ctx.user_id,
                input.message_id,
                &input.emoji,
            )
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.room_id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(commit.summary)
    }

    async fn remove_reaction(
        &self,
        ctx: ChatContext,
        input: RemoveReaction,
    ) -> ChatResult<ReactionSummary> {
        let pool = self.pool_for(&ctx).await?;
        let commit = self
            .repo
            .remove_reaction(
                &pool,
                &ctx.organization_id,
                &ctx.user_id,
                input.message_id,
                &input.emoji,
            )
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.room_id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(commit.summary)
    }

    async fn list_members(&self, ctx: ChatContext, room_id: RoomId) -> ChatResult<Vec<MemberView>> {
        let pool = self.pool_for(&ctx).await?;
        self.members
            .list_members(&pool, &ctx.organization_id, &ctx.user_id, room_id)
            .await
    }

    async fn add_members(
        &self,
        ctx: ChatContext,
        input: AddMembers,
    ) -> ChatResult<Vec<MemberView>> {
        let pool = self.pool_for(&ctx).await?;
        let commit = self
            .members
            .add_members(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.event.room_id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(commit.members)
    }

    async fn remove_member(
        &self,
        ctx: ChatContext,
        input: RemoveMember,
    ) -> ChatResult<RemoveResult> {
        let pool = self.pool_for(&ctx).await?;
        let user_id = input.user_id.clone();
        let commit = self
            .members
            .remove_member(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await?;
        self.publisher
            .publish_room_event(&ctx.client_name, commit.event.room_id, commit.event)
            .await;
        if let Some(outbox_id) = commit.outbox_id {
            self.repo.mark_outbox_published(&pool, outbox_id).await?;
        }
        Ok(RemoveResult { ok: true, user_id })
    }

    async fn search_messages(
        &self,
        ctx: ChatContext,
        input: SearchMessages,
    ) -> ChatResult<SearchPage> {
        let pool = self.pool_for(&ctx).await?;
        self.search
            .search_messages(&pool, &ctx.organization_id, &ctx.user_id, input)
            .await
    }

    async fn publish_pending_outbox(&self, limit_per_pool: usize) -> ChatResult<u64> {
        let mut published = 0_u64;
        for (client_name, pool) in self.pools.list_pools().await? {
            schema::ensure_chat_runtime_schema(&pool, &client_name, &self.schema_ready)
                .await
                .map_err(|err| {
                    ChatError::Internal(format!(
                        "chat outbox schema ensure failed for client '{client_name}': {err}"
                    ))
                })?;
            let leased = self
                .repo
                .lease_pending_outbox(&pool, limit_per_pool as i64)
                .await
                .map_err(|err| {
                    ChatError::Internal(format!(
                        "chat outbox lease failed for client '{client_name}': {err}"
                    ))
                })?;
            for (row_id, room_id, event) in leased {
                self.publisher
                    .publish_room_event(&client_name, room_id, event)
                    .await;
                self.repo
                    .mark_outbox_published(&pool, row_id)
                    .await
                    .map_err(|err| {
                        ChatError::Internal(format!(
                            "chat outbox publish finalize failed for client '{client_name}', row '{row_id}': {err}"
                        ))
                    })?;
                published += 1;
            }
        }
        Ok(published)
    }
}

/// Stub [`ChatApp`] that returns [`ChatError::Unavailable`] for all operations except outbox drain.
#[derive(Default)]
pub struct NoopChatApp;

#[async_trait]
impl ChatApp for NoopChatApp {
    async fn create_room(&self, _ctx: ChatContext, _input: CreateRoom) -> ChatResult<RoomView> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn list_rooms(&self, _ctx: ChatContext, _input: ListRooms) -> ChatResult<RoomPage> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn get_room(&self, _ctx: ChatContext, _room_id: RoomId) -> ChatResult<RoomView> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn update_room(&self, _ctx: ChatContext, _input: UpdateRoom) -> ChatResult<RoomView> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn archive_room(&self, _ctx: ChatContext, _input: ArchiveRoom) -> ChatResult<RoomView> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn list_messages(
        &self,
        _ctx: ChatContext,
        _input: ListMessages,
    ) -> ChatResult<MessagePage> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn send_message(
        &self,
        _ctx: ChatContext,
        _input: SendMessage,
    ) -> ChatResult<MessageView> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn edit_message(
        &self,
        _ctx: ChatContext,
        _input: EditMessage,
    ) -> ChatResult<MessageView> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn delete_message(
        &self,
        _ctx: ChatContext,
        _input: DeleteMessage,
    ) -> ChatResult<DeleteResult> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn mark_read_up_to(
        &self,
        _ctx: ChatContext,
        _input: MarkReadUpTo,
    ) -> ChatResult<ReadCursorView> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn add_reaction(
        &self,
        _ctx: ChatContext,
        _input: AddReaction,
    ) -> ChatResult<ReactionSummary> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn remove_reaction(
        &self,
        _ctx: ChatContext,
        _input: RemoveReaction,
    ) -> ChatResult<ReactionSummary> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn list_members(
        &self,
        _ctx: ChatContext,
        _room_id: RoomId,
    ) -> ChatResult<Vec<MemberView>> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn add_members(
        &self,
        _ctx: ChatContext,
        _input: AddMembers,
    ) -> ChatResult<Vec<MemberView>> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn remove_member(
        &self,
        _ctx: ChatContext,
        _input: RemoveMember,
    ) -> ChatResult<RemoveResult> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn search_messages(
        &self,
        _ctx: ChatContext,
        _input: SearchMessages,
    ) -> ChatResult<SearchPage> {
        Err(ChatError::Unavailable(
            "chat app not configured".to_string(),
        ))
    }
    async fn publish_pending_outbox(&self, _limit_per_pool: usize) -> ChatResult<u64> {
        Ok(0)
    }
}
