// bot/src/db/cardRepository.ts
// S-07 (invariante): nenhum campo de Card ou CardStage armazena tokens,
// API keys ou passwords. Qualquer adição futura de campo deve respeitar
// esta restrição e passar por revisão de segurança.

import type { Statement } from 'better-sqlite3'
import { db } from './index'
import type { Card, CardProvider, CardStage, CardStatus, StageName } from '../types'

// ── Erros de domínio ──────────────────────────────────────────────────────────

export class AccessDeniedError extends Error {
  constructor(cardId: string) {
    super(`Acesso negado: você não é o criador do card ${cardId}`)
    this.name = 'AccessDeniedError'
  }
}

export class CardNotFoundError extends Error {
  constructor(cardId: string) {
    super(`Card não encontrado: ${cardId}`)
    this.name = 'CardNotFoundError'
  }
}

// ── Repositório ───────────────────────────────────────────────────────────────

export class CardRepository {
  // Prepared statements — preparados no construtor para performance
  private readonly stmtInsertCard: Statement
  private readonly stmtUpdateStatus: Statement
  private readonly stmtTouchUpdate: Statement
  private readonly stmtUpsertStage: Statement
  private readonly stmtGetCard: Statement
  private readonly stmtListByUser: Statement
  private readonly stmtGetStages: Statement
  private readonly stmtGetByChannel: Statement

  constructor() {
    this.stmtInsertCard = db.prepare(`
      INSERT OR IGNORE INTO cards
        (card_id, provider, external_url, slack_user_id, slack_channel_id, titulo, texto_original, status)
      VALUES
        (@card_id, @provider, @external_url, @slack_user_id, @slack_channel_id, @titulo, @texto_original, @status)
    `)

    this.stmtUpdateStatus = db.prepare(`
      UPDATE cards SET status = @status WHERE card_id = @card_id
    `)

    this.stmtTouchUpdate = db.prepare(`
      UPDATE cards SET texto_original = @texto_original WHERE card_id = @card_id
    `)

    // INSERT OR REPLACE recria a linha: o trigger cards_set_atualizado_em
    // não dispara neste caso, mas gerado_em usa DEFAULT (strftime now) apenas
    // na inserção. Para garantir gerado_em atualizado usamos o mesmo mecanismo
    // do schema: UNIQUE(card_id, stage) + INSERT OR REPLACE recria o registro
    // com o DEFAULT de gerado_em avaliado no momento do REPLACE.
    this.stmtUpsertStage = db.prepare(`
      INSERT OR REPLACE INTO card_stages (card_id, stage, conteudo)
      VALUES (@card_id, @stage, @conteudo)
    `)

    this.stmtGetCard = db.prepare(`
      SELECT * FROM cards WHERE card_id = ?
    `)

    this.stmtListByUser = db.prepare(`
      SELECT card_id, titulo FROM cards
      WHERE slack_user_id = ?
        AND status != 'aprovado'
        AND deleted_at IS NULL
      ORDER BY criado_em DESC
    `)

    this.stmtGetStages = db.prepare(`
      SELECT * FROM card_stages WHERE card_id = ? ORDER BY id ASC
    `)

    this.stmtGetByChannel = db.prepare(`
      SELECT card_id, titulo FROM cards
      WHERE slack_channel_id = ?
        AND deleted_at IS NULL
        AND status != 'aprovado'
    `)
  }

  // ── Mutações ─────────────────────────────────────────────────────────────────

  /**
   * Insere um card de forma idempotente.
   * INSERT OR IGNORE: uma segunda chamada com o mesmo card_id é ignorada silenciosamente.
   */
  insertCard(
    c: Omit<Card, 'criado_em' | 'atualizado_em' | 'deleted_at' | 'provider' | 'external_url'> &
      Partial<Pick<Card, 'provider' | 'external_url'>>
  ): void {
    this.stmtInsertCard.run({
      ...c,
      provider: c.provider ?? inferProvider(c.card_id),
      external_url: c.external_url ?? null,
    })
  }

  /** Atualiza o status do card. */
  updateStatus(cardId: string, status: CardStatus): void {
    this.stmtUpdateStatus.run({ card_id: cardId, status })
  }

  /**
   * Atualiza o texto original do card.
   * atualizado_em é gerenciado pelo trigger cards_set_atualizado_em no banco.
   */
  touchUpdate(cardId: string, textoOriginal: string): void {
    this.stmtTouchUpdate.run({ card_id: cardId, texto_original: textoOriginal })
  }

  /**
   * Insere ou substitui um estágio para o card.
   * A combinação (card_id, stage) é única; em caso de duplicata o registro
   * é substituído e gerado_em recebe o DEFAULT (timestamp atual).
   */
  upsertStage(cardId: string, stage: StageName, conteudo: string): void {
    this.stmtUpsertStage.run({ card_id: cardId, stage, conteudo })
  }

  // ── Leitura ──────────────────────────────────────────────────────────────────

  /** Retorna o card pelo ID, ou undefined se não existir. */
  getCard(cardId: string): Card | undefined {
    return this.stmtGetCard.get(cardId) as Card | undefined
  }

  /**
   * Verifica existência e ownership do card.
   * - Lança CardNotFoundError se o card não existir.
   * - Lança AccessDeniedError se o criador for diferente de userId.
   * - Retorna o card quando as verificações passam.
   *
   * assertOwner é a única função que lança AccessDeniedError;
   * handlers de comando devem chamar apenas assertOwner para verificar acesso.
   */
  assertOwner(cardId: string, userId: string): Card {
    const card = this.getCard(cardId)
    if (!card) throw new CardNotFoundError(cardId)
    if (card.slack_user_id !== userId) throw new AccessDeniedError(cardId)
    return card
  }

  /**
   * Lista cards ativos do usuário (excluindo aprovados e deletados).
   */
  listByUser(userId: string): Pick<Card, 'card_id' | 'titulo'>[] {
    return this.stmtListByUser.all(userId) as Pick<Card, 'card_id' | 'titulo'>[]
  }

  /** Retorna todos os estágios do card na ordem de inserção. */
  getStages(cardId: string): CardStage[] {
    return this.stmtGetStages.all(cardId) as CardStage[]
  }

  /**
   * Retorna o card ativo do canal, ou undefined se não houver.
   * Usado para evitar violação de UNIQUE(slack_channel_id) em create.ts.
   */
  getCardByChannel(channelId: string): Pick<Card, 'card_id' | 'titulo'> | undefined {
    return this.stmtGetByChannel.get(channelId) as Pick<Card, 'card_id' | 'titulo'> | undefined
  }
}

export const cardRepository = new CardRepository()

function inferProvider(cardId: string): CardProvider {
  return cardId.startsWith('GH-') ? 'github' : 'jira'
}
