/**
 * KB review-queue repository — settings-kb (wave-14).
 *
 * Handles the PENDING_REVIEW article lifecycle:
 *  - listPendingReview — paginated list of articles awaiting review
 *  - submitForReview — transitions DRAFT → PENDING_REVIEW (non-admin/owner authors)
 *  - approveArticle  — transitions PENDING_REVIEW → PUBLISHED (admin/owner)
 *  - rejectArticle   — transitions PENDING_REVIEW → DRAFT (admin/owner)
 */
import { eq, and, asc, count, desc } from 'drizzle-orm'
import type { Db } from '../client'
import { kbArticles, kbSpaces, users } from '../schema'
import { auditLog } from '../schema/audit-log'

// ── Types ─────────────────────────────────────────────────────────────────────

export interface KbReviewItem {
  id: string
  tenantId: string
  spaceId: string
  spaceName: string
  title: string
  slug: string
  status: string
  submittedAt: string
  createdBy: string
  createdByName: string
}

export interface KbReviewPage {
  items: KbReviewItem[]
  total: number
  page: number
  pageSize: number
}

export interface KbArticleRejection {
  feedback: string | null
  rejectedAt: string
  rejectedBy: string
}

// ── Typed errors ──────────────────────────────────────────────────────────────

export class KbArticleNotFoundError extends Error {
  constructor() {
    super('Article not found')
    this.name = 'KbArticleNotFoundError'
  }
}

export class KbArticleStatusError extends Error {
  constructor(msg: string) {
    super(msg)
    this.name = 'KbArticleStatusError'
  }
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function assertArticle(
  row: { id: string; status: string } | undefined,
): asserts row is { id: string; status: string } {
  if (!row) throw new KbArticleNotFoundError()
}

function extractRejectionFeedback(
  changes: Record<string, [unknown, unknown]> | null | undefined,
): string | null {
  const entry = changes?.feedback
  if (!entry || !Array.isArray(entry)) return null
  const value = entry[1]
  return typeof value === 'string' ? value : null
}

// ── Queries ───────────────────────────────────────────────────────────────────

/**
 * List articles in PENDING_REVIEW status for a tenant, oldest first.
 */
export async function listPendingReview(
  db: Db,
  tenantId: string,
  page = 1,
  pageSize = 20,
): Promise<KbReviewPage> {
  const offset = (page - 1) * pageSize

  const rows = await db
    .select({
      id: kbArticles.id,
      tenantId: kbArticles.tenantId,
      spaceId: kbArticles.spaceId,
      spaceName: kbSpaces.name,
      title: kbArticles.title,
      slug: kbArticles.slug,
      status: kbArticles.status,
      updatedAt: kbArticles.updatedAt,
      createdBy: kbArticles.createdBy,
      createdByName: users.name,
    })
    .from(kbArticles)
    .innerJoin(kbSpaces, eq(kbArticles.spaceId, kbSpaces.id))
    .innerJoin(users, eq(kbArticles.createdBy, users.id))
    .where(and(eq(kbArticles.tenantId, tenantId), eq(kbArticles.status, 'PENDING_REVIEW')))
    .orderBy(asc(kbArticles.updatedAt))
    .limit(pageSize)
    .offset(offset)

  const countResult = await db
    .select({ total: count(kbArticles.id) })
    .from(kbArticles)
    .where(and(eq(kbArticles.tenantId, tenantId), eq(kbArticles.status, 'PENDING_REVIEW')))
  const total = Number(countResult[0]?.total ?? 0)

  return {
    items: rows.map((r) => ({
      id: r.id,
      tenantId: r.tenantId,
      spaceId: r.spaceId,
      spaceName: r.spaceName,
      title: r.title,
      slug: r.slug,
      status: r.status,
      submittedAt: r.updatedAt instanceof Date ? r.updatedAt.toISOString() : String(r.updatedAt),
      createdBy: r.createdBy,
      createdByName: r.createdByName ?? '',
    })),
    total: Number(total),
    page,
    pageSize,
  }
}

/**
 * Return the most recent rejection audit entry for an article, if any.
 */
export async function getLatestRejectionFeedback(
  db: Db,
  tenantId: string,
  articleId: string,
): Promise<KbArticleRejection | null> {
  const [row] = await db
    .select({
      changes: auditLog.changes,
      rejectedAt: auditLog.createdAt,
      rejectedBy: auditLog.actorId,
    })
    .from(auditLog)
    .where(
      and(
        eq(auditLog.tenantId, tenantId),
        eq(auditLog.entityType, 'kb_article'),
        eq(auditLog.entityId, articleId),
        eq(auditLog.action, 'rejected'),
      ),
    )
    .orderBy(desc(auditLog.createdAt))
    .limit(1)

  if (!row || !row.rejectedBy) return null

  return {
    feedback: extractRejectionFeedback(row.changes),
    rejectedAt:
      row.rejectedAt instanceof Date ? row.rejectedAt.toISOString() : String(row.rejectedAt),
    rejectedBy: row.rejectedBy,
  }
}

/**
 * Submit an article for review: DRAFT → PENDING_REVIEW.
 * Only allowed when the article is in DRAFT status.
 */
export async function submitForReview(
  db: Db,
  tenantId: string,
  articleId: string,
): Promise<void> {
  const [article] = await db
    .select({ id: kbArticles.id, status: kbArticles.status })
    .from(kbArticles)
    .where(and(eq(kbArticles.id, articleId), eq(kbArticles.tenantId, tenantId)))
    .limit(1)

  assertArticle(article)

  if (article.status !== 'DRAFT') {
    throw new KbArticleStatusError(`Cannot submit for review: article is ${article.status}`)
  }

  await db
    .update(kbArticles)
    .set({ status: 'PENDING_REVIEW', updatedAt: new Date() })
    .where(and(eq(kbArticles.id, articleId), eq(kbArticles.tenantId, tenantId)))
}

/**
 * Approve an article: PENDING_REVIEW → PUBLISHED.
 * Sets publishedAt when transitioning for the first time.
 */
export async function approveArticle(
  db: Db,
  tenantId: string,
  articleId: string,
  reviewerId: string,
): Promise<void> {
  const [article] = await db
    .select({ id: kbArticles.id, status: kbArticles.status, publishedAt: kbArticles.publishedAt })
    .from(kbArticles)
    .where(and(eq(kbArticles.id, articleId), eq(kbArticles.tenantId, tenantId)))
    .limit(1)

  assertArticle(article)

  if (article.status !== 'PENDING_REVIEW') {
    throw new KbArticleStatusError(`Cannot approve: article is ${article.status}`)
  }

  const now = new Date()
  await db
    .update(kbArticles)
    .set({
      status: 'PUBLISHED',
      publishedAt: article.publishedAt ?? now,
      updatedAt: now,
      updatedBy: reviewerId,
    })
    .where(and(eq(kbArticles.id, articleId), eq(kbArticles.tenantId, tenantId)))
}

/**
 * Reject an article: PENDING_REVIEW → DRAFT.
 * Optional feedback is recorded in the audit trail for the author.
 */
export async function rejectArticle(
  db: Db,
  tenantId: string,
  articleId: string,
  reviewerId: string,
  feedback?: string | null,
): Promise<void> {
  const [article] = await db
    .select({ id: kbArticles.id, status: kbArticles.status })
    .from(kbArticles)
    .where(and(eq(kbArticles.id, articleId), eq(kbArticles.tenantId, tenantId)))
    .limit(1)

  assertArticle(article)

  if (article.status !== 'PENDING_REVIEW') {
    throw new KbArticleStatusError(`Cannot reject: article is ${article.status}`)
  }

  const trimmedFeedback = feedback?.trim() || null

  await db.transaction(async (tx) => {
    await tx
      .update(kbArticles)
      .set({ status: 'DRAFT', updatedAt: new Date(), updatedBy: reviewerId })
      .where(and(eq(kbArticles.id, articleId), eq(kbArticles.tenantId, tenantId)))

    await tx.insert(auditLog).values({
      tenantId,
      actorId: reviewerId,
      actorType: 'user',
      entityType: 'kb_article',
      entityId: articleId,
      action: 'rejected',
      changes: trimmedFeedback ? { feedback: [null, trimmedFeedback] } : null,
    })
  })
}
