/**
 * Chat thread + message persistence.
 *
 * Each ChatThread holds an ordered, monotonic sequence of ChatMessages
 * (turnIndex 0..N). The schema's `@@unique([threadId, turnIndex])` prevents
 * concurrent appenders from clobbering each other; on collision the caller
 * retries with a fresh `nextTurnIndex(threadId)`.
 *
 * `status` is `'pending' | 'complete' | 'error'`. Orchestrators flip a row
 * from `pending` → `complete` once the turn settles. A crash mid-turn leaves
 * a `pending` row that downstream views should render as truncated.
 */
import { Prisma } from '@prisma/client';
import type { PrismaClient, ChatThread, ChatMessage } from '@prisma/client';

export type ChatRole = 'system' | 'user' | 'assistant' | 'tool';
export type ChatStatus = 'pending' | 'complete' | 'error';

export interface AppendMessageInput {
  threadId: string;
  role: ChatRole;
  content: string;
  toolCalls?: Array<Record<string, unknown>>;
  toolCallId?: string;
  status?: ChatStatus;
  /** Optional explicit turnIndex (caller-provided). If omitted the repo allocates the next one. */
  turnIndex?: number;
}

/** A thread is scoped to an Agent XOR a Project (exactly one id set). */
export interface CreateThreadInput {
  agentId?: string;
  projectId?: string;
  ownerId: string;
  title?: string;
}

export interface IChatRepository {
  createThread(input: CreateThreadInput): Promise<ChatThread>;
  findThread(id: string): Promise<ChatThread | null>;
  listThreadsByAgent(agentId: string, ownerId?: string): Promise<ChatThread[]>;
  listThreadsByProject(projectId: string, ownerId?: string): Promise<ChatThread[]>;
  deleteThread(id: string): Promise<void>;
  appendMessage(input: AppendMessageInput): Promise<ChatMessage>;
  listMessages(threadId: string): Promise<ChatMessage[]>;
  updateStatus(messageId: string, status: ChatStatus): Promise<ChatMessage>;
  markPendingAsError(threadId: string): Promise<number>;
  touchThread(threadId: string): Promise<void>;
  /** Compute MAX(turnIndex)+1 for a thread. 0 if the thread is empty. */
  nextTurnIndex(threadId: string): Promise<number>;
}

const RACE_RETRIES = 3;
/** Postgres unique-constraint violation code (Prisma surfaces it as P2002). */
const UNIQUE_VIOLATION = 'P2002';

export class ChatRepository implements IChatRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async createThread(input: CreateThreadInput): Promise<ChatThread> {
    return this.prisma.chatThread.create({
      data: {
        agentId: input.agentId ?? null,
        projectId: input.projectId ?? null,
        ownerId: input.ownerId,
        title: input.title ?? '',
      },
    });
  }

  async findThread(id: string): Promise<ChatThread | null> {
    return this.prisma.chatThread.findUnique({ where: { id } });
  }

  async listThreadsByAgent(agentId: string, ownerId?: string): Promise<ChatThread[]> {
    return this.prisma.chatThread.findMany({
      where: { agentId, ...(ownerId !== undefined ? { ownerId } : {}) },
      orderBy: { lastTurnAt: 'desc' },
    });
  }

  async listThreadsByProject(projectId: string, ownerId?: string): Promise<ChatThread[]> {
    return this.prisma.chatThread.findMany({
      where: { projectId, ...(ownerId !== undefined ? { ownerId } : {}) },
      orderBy: { lastTurnAt: 'desc' },
    });
  }

  async deleteThread(id: string): Promise<void> {
    // Messages cascade via the ChatMessage.thread FK (onDelete: Cascade).
    await this.prisma.chatThread.delete({ where: { id } });
  }

  async listMessages(threadId: string): Promise<ChatMessage[]> {
    return this.prisma.chatMessage.findMany({
      where: { threadId },
      orderBy: { turnIndex: 'asc' },
    });
  }

  async nextTurnIndex(threadId: string): Promise<number> {
    const last = await this.prisma.chatMessage.findFirst({
      where: { threadId },
      orderBy: { turnIndex: 'desc' },
      select: { turnIndex: true },
    });
    return (last?.turnIndex ?? -1) + 1;
  }

  async appendMessage(input: AppendMessageInput): Promise<ChatMessage> {
    let attempt = 0;
    // Retry on unique-violation: parallel appenders can both compute the same
    // nextTurnIndex; the second insert fails P2002 and we recompute + retry.
    while (true) {
      const turnIndex = input.turnIndex ?? (await this.nextTurnIndex(input.threadId));
      try {
        return await this.prisma.chatMessage.create({
          data: {
            threadId: input.threadId,
            turnIndex,
            role: input.role,
            content: input.content,
            toolCalls: input.toolCalls === undefined
              ? Prisma.JsonNull
              : (input.toolCalls as Prisma.InputJsonValue),
            toolCallId: input.toolCallId ?? null,
            status: input.status ?? 'complete',
          },
        });
      } catch (err) {
        attempt += 1;
        const code = (err as { code?: string }).code;
        if (code === UNIQUE_VIOLATION && input.turnIndex === undefined && attempt <= RACE_RETRIES) {
          continue;
        }
        throw err;
      }
    }
  }

  async updateStatus(messageId: string, status: ChatStatus): Promise<ChatMessage> {
    return this.prisma.chatMessage.update({
      where: { id: messageId },
      data: { status },
    });
  }

  async markPendingAsError(threadId: string): Promise<number> {
    const res = await this.prisma.chatMessage.updateMany({
      where: { threadId, status: 'pending' },
      data: { status: 'error' },
    });
    return res.count;
  }

  async touchThread(threadId: string): Promise<void> {
    await this.prisma.chatThread.update({
      where: { id: threadId },
      data: { lastTurnAt: new Date() },
    });
  }
}
