import type { Querier } from '@platform-modules/db'
import { assertCanModerate } from './authz.js'
import { CommentModerationError, CommentNotFoundError } from './errors.js'
import { normalizeCommentInput } from './model.js'
import { assertSanitize, getById, insertComment, setStatus } from './store.js'
import type { CommentsSchema } from './schema.js'
import type {
  Actor,
  Comment,
  CommentAuthor,
  CommentInput,
  CommentStatus,
  CommentTarget,
  Sanitize,
  StoreOpts,
} from './types.js'

// Typed errors a ./moderation-only consumer can encounter (reachable for isCommentError + catch-by-type).
export { CommentModerationError, CommentNotFoundError, CommentSanitizationError, isCommentError } from './errors.js'

/** Default classify timeout — fail-safe lands pending on hang (spec §2). */
export const CLASSIFY_TIMEOUT_MS = 10_000

type ModeratableStatus = Extract<CommentStatus, 'published' | 'pending' | 'spam'>

const MODERATABLE_STATUSES: ReadonlySet<string> = new Set(['published', 'pending', 'spam'])

/**
 * Trust-boundary guard for an injected moderator's verdict. The CommentModerator is an external
 * authority (an ai service, a custom classifier) — its return value is untrusted. A resolved-but-
 * out-of-contract status (undefined/null/'trashed'/'approved'/any string) is NOT caught by the
 * throw/timeout fail-safe and would otherwise be written verbatim to the status column (no DB CHECK).
 */
function isModeratableStatus(s: unknown): s is ModeratableStatus {
  return typeof s === 'string' && MODERATABLE_STATUSES.has(s)
}

export interface CommentModerator {
  classify(c: {
    body: string
    author: CommentAuthor
    target: CommentTarget
  }): Promise<{ status: ModeratableStatus; reason?: string }>
}

export interface AiClassifyClient {
  classify(input: {
    body: string
    author: CommentAuthor
    target: CommentTarget
  }): Promise<{ spamScore: number }>
}

function classifyWithTimeout(
  moderator: CommentModerator,
  input: { body: string; author: CommentAuthor; target: CommentTarget },
): Promise<{ status: ModeratableStatus; reason?: string }> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error('classify_timeout')), CLASSIFY_TIMEOUT_MS)
    moderator
      .classify(input)
      .then((result) => {
        clearTimeout(timer)
        resolve(result)
      })
      .catch((err: unknown) => {
        clearTimeout(timer)
        reject(err)
      })
  })
}

async function resolveModeratedStatus(
  moderator: CommentModerator,
  input: { body: string; author: CommentAuthor; target: CommentTarget },
  target: CommentTarget,
): Promise<ModeratableStatus> {
  try {
    const verdict = await classifyWithTimeout(moderator, input)
    // Fail-safe toward review: a resolved-but-malformed verdict (non-{published,pending,spam})
    // must NOT be written verbatim — treat it as a classifier failure → pending (spec §0 hard floor).
    if (!isModeratableStatus(verdict?.status)) {
      throw new CommentModerationError(`invalid verdict status: ${JSON.stringify(verdict?.status)}`)
    }
    return verdict.status
  } catch (reason) {
    console.warn({
      event: 'comment_moderation_failed',
      reason: reason instanceof Error ? reason.message : String(reason),
      targetType: target.type,
      targetId: target.id,
    })
    return 'pending'
  }
}

/**
 * postModerated — classify then insert with moderator verdict (system authority).
 * Fail-safe: throw/timeout → pending, NEVER published.
 */
export async function postModerated(
  db: Querier<CommentsSchema>,
  input: CommentInput,
  actor: Actor | null,
  sanitize: Sanitize,
  moderator: CommentModerator,
  opts?: StoreOpts,
): Promise<Comment> {
  assertSanitize(sanitize, 'postModerated')
  const normalized = normalizeCommentInput(input)

  const status = await resolveModeratedStatus(
    moderator,
    { body: normalized.body, author: normalized.author, target: normalized.target },
    normalized.target,
  )

  const ref = await insertComment(db, normalized, status, sanitize, opts)
  const comment = await getById(db, ref.id, { id: actor?.id ?? 'system', canModerate: true }, opts)
  if (!comment) throw new CommentNotFoundError(`id=${ref.id}`)
  return comment
}

/**
 * moderate — re-classify an existing comment (moderator queue).
 * Classifier errors propagate; existing status unchanged on failure.
 */
export async function moderate(
  db: Querier<CommentsSchema>,
  id: string,
  moderator: CommentModerator,
  actor: Actor,
  opts?: StoreOpts,
): Promise<Comment> {
  assertCanModerate(actor, 'moderate', id)

  const existing = await getById(db, id, actor, opts)
  if (!existing) throw new CommentNotFoundError(`id=${id}`)

  const verdict = await moderator.classify({
    body: existing.body,
    author: existing.author,
    target: existing.target,
  })

  // Untrusted classifier return — reject an out-of-contract status rather than write it verbatim;
  // the existing row's status is left unchanged (parity with the "classifier throw" path).
  if (!isModeratableStatus(verdict?.status)) {
    throw new CommentModerationError(`invalid verdict status: ${JSON.stringify(verdict?.status)}`)
  }

  await setStatus(db, id, verdict.status, actor, opts)
  const updated = await getById(db, id, actor, opts)
  if (!updated) throw new CommentNotFoundError(`id=${id}`)
  return updated
}

/** Reference ai-backed moderator — client errors propagate to caller fail-safe. */
export function aiModerator(client: AiClassifyClient, opts?: { threshold?: number }): CommentModerator {
  const threshold = opts?.threshold ?? 0.5
  return {
    async classify(input) {
      const { spamScore } = await client.classify(input)
      // Fail-CLOSED on a malformed score (spec §0 hard floor): a classifier that resolves with a
      // non-finite/non-numeric spamScore ({}, undefined, null, NaN, a string, an error body) must
      // NOT default to 'published'. `>=` silently coerces all of those to false → 'published',
      // auto-publishing unmoderated content on a routine partial/rate-limited API response. Throw
      // instead → routes through postModerated's pending fail-safe (held for review, NEVER published).
      if (typeof spamScore !== 'number' || !Number.isFinite(spamScore)) {
        throw new CommentModerationError(
          `invalid spamScore: ${typeof spamScore === 'number' ? spamScore : typeof spamScore}`,
        )
      }
      return { status: spamScore >= threshold ? 'spam' : 'published' }
    },
  }
}
