import { and, asc, desc, eq, gt, ilike, inArray, isNull, lt, lte, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { agents, chatConversations, chatFiles, chatMessageBookmarks, chatMessages } from "@paperclipai/db";
import { notFound } from "../errors.js";

export function chatService(db: Db) {
  return {
    async listConversations(
      companyId: string,
      opts: { cursor?: string; limit?: number; includeArchived?: boolean; search?: string; agentId?: string },
    ) {
      const limit = Math.min(opts.limit ?? 30, 100);
      const includeArchived = opts.includeArchived ?? false;

      const conditions = [
        eq(chatConversations.companyId, companyId),
        isNull(chatConversations.deletedAt),
      ];

      if (!includeArchived) {
        conditions.push(isNull(chatConversations.archivedAt));
      }

      if (opts.cursor) {
        conditions.push(lt(chatConversations.updatedAt, new Date(opts.cursor)));
      }

      if (opts.search) {
        conditions.push(ilike(chatConversations.title, `%${opts.search}%`));
      }

      if (opts.agentId) {
        conditions.push(eq(chatConversations.agentId, opts.agentId));
      }

      const rows = await db
        .select()
        .from(chatConversations)
        .where(and(...conditions))
        .orderBy(desc(chatConversations.updatedAt))
        .limit(limit + 1);

      const hasMore = rows.length > limit;
      const items = hasMore ? rows.slice(0, limit) : rows;
      const nextCursor = hasMore ? items[items.length - 1]!.updatedAt.toISOString() : null;

      return { items, hasMore, nextCursor };
    },

    async createConversation(companyId: string, data: { title?: string; agentId?: string }) {
      const [row] = await db
        .insert(chatConversations)
        .values({
          companyId,
          title: data.title ?? null,
          agentId: data.agentId ?? null,
        })
        .returning();
      return row!;
    },

    async getConversation(id: string) {
      const [row] = await db
        .select()
        .from(chatConversations)
        .where(and(eq(chatConversations.id, id), isNull(chatConversations.deletedAt)));
      if (!row) throw notFound("Conversation not found");
      return row;
    },

    async updateConversation(
      id: string,
      data: {
        title?: string;
        agentId?: string | null;
        pinnedAt?: string | null;
        archivedAt?: string | null;
      },
    ) {
      const patch: Record<string, unknown> = { updatedAt: new Date() };

      if (data.title !== undefined) patch.title = data.title;
      if (data.agentId !== undefined) patch.agentId = data.agentId;
      if ("pinnedAt" in data) {
        patch.pinnedAt = data.pinnedAt ? new Date(data.pinnedAt) : null;
      }
      if ("archivedAt" in data) {
        patch.archivedAt = data.archivedAt ? new Date(data.archivedAt) : null;
      }

      const [row] = await db
        .update(chatConversations)
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        .set(patch as any)
        .where(eq(chatConversations.id, id))
        .returning();

      return row!;
    },

    async softDeleteConversation(id: string) {
      const now = new Date();
      const [row] = await db
        .update(chatConversations)
        .set({ deletedAt: now, updatedAt: now })
        .where(and(eq(chatConversations.id, id), isNull(chatConversations.deletedAt)))
        .returning();
      if (!row) throw notFound("Conversation not found");
      return row;
    },

    async listMessages(
      conversationId: string,
      opts: { cursor?: string; limit?: number },
    ) {
      const limit = Math.min(opts.limit ?? 50, 200);

      const conditions = [eq(chatMessages.conversationId, conversationId)];

      if (opts.cursor) {
        conditions.push(lt(chatMessages.createdAt, new Date(opts.cursor)));
      }

      const rows = await db
        .select()
        .from(chatMessages)
        .where(and(...conditions))
        .orderBy(desc(chatMessages.createdAt))
        .limit(limit + 1);

      const hasMore = rows.length > limit;
      const items = hasMore ? rows.slice(0, limit) : rows;
      const nextCursor = hasMore ? items[items.length - 1]!.createdAt.toISOString() : null;

      // Load files for these messages and attach them
      const messageIds = items.map((m) => m.id);
      const filesByMessageId = new Map<string, typeof chatFiles.$inferSelect[]>();
      if (messageIds.length > 0) {
        try {
          const fileRows = await db
            .select()
            .from(chatFiles)
            .where(inArray(chatFiles.messageId, messageIds))
            .orderBy(asc(chatFiles.createdAt));
          for (const f of fileRows) {
            if (!f.messageId) continue;
            const existing = filesByMessageId.get(f.messageId) ?? [];
            existing.push(f);
            filesByMessageId.set(f.messageId, existing);
          }
        } catch {
          // chat_files table may not exist yet — treat as no files
        }
      }

      const itemsWithFiles = items.map((m) => ({
        ...m,
        files: (filesByMessageId.get(m.id) ?? []).map((f) => ({
          ...f,
          createdAt: f.createdAt.toISOString(),
          updatedAt: f.updatedAt.toISOString(),
        })),
      }));

      return { items: itemsWithFiles, hasMore, nextCursor };
    },

    async addMessage(
      conversationId: string,
      data: { role: string; content: string; agentId?: string; messageType?: string },
    ) {
      const [message] = await db
        .insert(chatMessages)
        .values({
          conversationId,
          role: data.role,
          content: data.content,
          agentId: data.agentId ?? null,
          messageType: data.messageType ?? null,
        })
        .returning();

      // Pitfall 3: Bump conversation updatedAt after inserting a message
      await db
        .update(chatConversations)
        .set({ updatedAt: new Date() })
        .where(eq(chatConversations.id, conversationId));

      // Pitfall 5: Auto-title from first user message (idempotent via IS NULL guard)
      if (data.role === "user") {
        await db
          .update(chatConversations)
          .set({ title: data.content.slice(0, 60), updatedAt: new Date() })
          .where(and(eq(chatConversations.id, conversationId), isNull(chatConversations.title)));
      }

      return { ...message!, files: [] };
    },

    async editMessage(messageId: string, content: string) {
      const [row] = await db
        .update(chatMessages)
        .set({ content })
        .where(eq(chatMessages.id, messageId))
        .returning();
      return row;
    },

    async truncateMessagesAfter(conversationId: string, messageId: string) {
      // Get the target message's createdAt
      const [target] = await db
        .select({ createdAt: chatMessages.createdAt })
        .from(chatMessages)
        .where(eq(chatMessages.id, messageId));
      if (!target) return;
      await db
        .delete(chatMessages)
        .where(
          and(
            eq(chatMessages.conversationId, conversationId),
            gt(chatMessages.createdAt, target.createdAt),
          ),
        );
    },

    async addSystemMessage(
      conversationId: string,
      data: { content: string; messageType: string; agentId?: string },
    ) {
      const [message] = await db
        .insert(chatMessages)
        .values({
          conversationId,
          role: "system",
          content: data.content,
          agentId: data.agentId ?? null,
          messageType: data.messageType,
        })
        .returning();

      // Bump conversation updatedAt (same pattern as addMessage)
      await db
        .update(chatConversations)
        .set({ updatedAt: new Date() })
        .where(eq(chatConversations.id, conversationId));

      return message!;
    },

    async *streamEcho(content: string, signal: AbortSignal) {
      const words = content.split(/\s+/);
      for (const word of words) {
        if (signal.aborted) break;
        await new Promise((r) => setTimeout(r, 50));
        yield word + " ";
      }
    },

    async searchMessages(
      companyId: string,
      query: string,
      opts: { limit?: number } = {},
    ) {
      if (!query.trim()) return { items: [] };

      const limit = Math.min(opts.limit ?? 20, 50);
      const tsQuery = sql`plainto_tsquery('english', ${query})`;
      const tsRank = sql`ts_rank("chat_messages"."content_search", plainto_tsquery('english', ${query}))`;

      const rows = await db
        .select({
          messageId: chatMessages.id,
          conversationId: chatMessages.conversationId,
          conversationTitle: chatConversations.title,
          content: chatMessages.content,
          role: chatMessages.role,
          agentId: chatMessages.agentId,
          createdAt: chatMessages.createdAt,
          rank: tsRank,
        })
        .from(chatMessages)
        .innerJoin(
          chatConversations,
          and(
            eq(chatMessages.conversationId, chatConversations.id),
            eq(chatConversations.companyId, companyId),
            isNull(chatConversations.deletedAt),
          ),
        )
        .where(sql`"chat_messages"."content_search" @@ ${tsQuery}`)
        .orderBy(desc(tsRank))
        .limit(limit);

      return {
        items: rows.map((r) => ({
          messageId: r.messageId,
          conversationId: r.conversationId,
          conversationTitle: r.conversationTitle,
          content: r.content,
          role: r.role as "user" | "assistant" | "system",
          agentId: r.agentId,
          createdAt: r.createdAt.toISOString(),
          rank: Number(r.rank),
        })),
      };
    },

    async toggleBookmark(companyId: string, messageId: string, conversationId: string) {
      return db.transaction(async (tx) => {
        const [existing] = await tx
          .select({ id: chatMessageBookmarks.id })
          .from(chatMessageBookmarks)
          .where(
            and(
              eq(chatMessageBookmarks.companyId, companyId),
              eq(chatMessageBookmarks.messageId, messageId),
            ),
          );

        if (existing) {
          await tx
            .delete(chatMessageBookmarks)
            .where(eq(chatMessageBookmarks.id, existing.id));
          return { bookmarked: false };
        } else {
          await tx
            .insert(chatMessageBookmarks)
            .values({ companyId, messageId, conversationId });
          return { bookmarked: true };
        }
      });
    },

    async getBookmarks(companyId: string, opts: { conversationId?: string } = {}) {
      const conditions = [eq(chatMessageBookmarks.companyId, companyId)];
      if (opts.conversationId) {
        conditions.push(eq(chatMessageBookmarks.conversationId, opts.conversationId));
      }

      const rows = await db
        .select({
          id: chatMessageBookmarks.id,
          companyId: chatMessageBookmarks.companyId,
          messageId: chatMessageBookmarks.messageId,
          conversationId: chatMessageBookmarks.conversationId,
          createdAt: chatMessageBookmarks.createdAt,
          messageRole: chatMessages.role,
          messageContent: chatMessages.content,
          messageAgentId: chatMessages.agentId,
          messageMessageType: chatMessages.messageType,
          messageCreatedAt: chatMessages.createdAt,
          messageUpdatedAt: chatMessages.updatedAt,
          conversationTitle: chatConversations.title,
        })
        .from(chatMessageBookmarks)
        .innerJoin(chatMessages, eq(chatMessageBookmarks.messageId, chatMessages.id))
        .innerJoin(chatConversations, eq(chatMessageBookmarks.conversationId, chatConversations.id))
        .where(and(...conditions))
        .orderBy(desc(chatMessageBookmarks.createdAt));

      return {
        items: rows.map((r) => ({
          id: r.id,
          companyId: r.companyId,
          messageId: r.messageId,
          conversationId: r.conversationId,
          createdAt: r.createdAt.toISOString(),
          message: {
            id: r.messageId,
            conversationId: r.conversationId,
            role: r.messageRole as "user" | "assistant" | "system",
            content: r.messageContent,
            agentId: r.messageAgentId,
            messageType: r.messageMessageType,
            createdAt: r.messageCreatedAt.toISOString(),
            updatedAt: r.messageUpdatedAt?.toISOString() ?? null,
          },
          conversationTitle: r.conversationTitle,
        })),
      };
    },

    async branchConversation(parentConversationId: string, branchFromMessageId: string, companyId: string) {
      // Get the branch point message
      const [branchMsg] = await db
        .select({ createdAt: chatMessages.createdAt })
        .from(chatMessages)
        .where(eq(chatMessages.id, branchFromMessageId));

      if (!branchMsg) throw notFound("Branch message not found");

      // Get all messages up to and including the branch point, ordered asc
      const messagesToCopy = await db
        .select()
        .from(chatMessages)
        .where(
          and(
            eq(chatMessages.conversationId, parentConversationId),
            lte(chatMessages.createdAt, branchMsg.createdAt),
          ),
        )
        .orderBy(asc(chatMessages.createdAt));

      // Create new child conversation
      const [newConversation] = await db
        .insert(chatConversations)
        .values({
          companyId,
          parentConversationId,
          branchFromMessageId,
          title: null,
          agentId: null,
        })
        .returning();

      // Copy messages into new conversation
      if (messagesToCopy.length > 0) {
        await db.insert(chatMessages).values(
          messagesToCopy.map(({ id: _id, conversationId: _convId, ...rest }) => ({
            ...rest,
            conversationId: newConversation!.id,
          })),
        );
      }

      return newConversation!;
    },

    async listBranches(conversationId: string) {
      return db
        .select()
        .from(chatConversations)
        .where(
          and(
            eq(chatConversations.parentConversationId, conversationId),
            isNull(chatConversations.deletedAt),
          ),
        )
        .orderBy(desc(chatConversations.createdAt));
    },

    async exportConversation(conversationId: string, format: "markdown" | "json") {
      // Note: loads all messages in memory. For very large conversations, consider streaming in future.
      const conversation = await this.getConversation(conversationId);

      const rows = await db
        .select({
          id: chatMessages.id,
          conversationId: chatMessages.conversationId,
          role: chatMessages.role,
          content: chatMessages.content,
          agentId: chatMessages.agentId,
          messageType: chatMessages.messageType,
          createdAt: chatMessages.createdAt,
          updatedAt: chatMessages.updatedAt,
          agentName: agents.name,
        })
        .from(chatMessages)
        .leftJoin(agents, eq(chatMessages.agentId, agents.id))
        .where(eq(chatMessages.conversationId, conversationId))
        .orderBy(asc(chatMessages.createdAt));

      const title = conversation.title ?? "Untitled";
      const dateStr = new Date(conversation.createdAt).toISOString().slice(0, 10);

      // Sanitize title for filename: lowercase, hyphens, no special chars, max 50
      const slug = title
        .toLowerCase()
        .replace(/\s+/g, "-")
        .replace(/[^a-z0-9-]/g, "")
        .slice(0, 50);

      if (format === "json") {
        const messages = rows.map((r) => ({
          id: r.id,
          conversationId: r.conversationId,
          role: r.role,
          content: r.content,
          agentId: r.agentId,
          agentName: r.agentName ?? null,
          messageType: r.messageType,
          createdAt: r.createdAt.toISOString(),
          updatedAt: r.updatedAt?.toISOString() ?? null,
        }));
        const content = JSON.stringify(
          { conversation: { id: conversation.id, title, createdAt: conversation.createdAt }, messages },
          null,
          2,
        );
        return { content, filename: `${slug}-${dateStr}.json` };
      }

      // Markdown format
      const exportDate = new Date().toISOString().slice(0, 10);
      let content = `# ${title}\nExported: ${exportDate}\n\n---\n\n`;
      for (const r of rows) {
        const speaker = r.role === "user" ? "You" : (r.agentName ?? r.role);
        const timestamp = r.createdAt.toISOString();
        content += `**${speaker}** (${timestamp})\n${r.content}\n\n---\n\n`;
      }

      return { content, filename: `${slug}-${dateStr}.md` };
    },
  };
}
