import { sql } from 'drizzle-orm'
import {
  type AnyPgColumn,
  boolean, integer, jsonb, numeric, pgEnum, pgPolicy, pgTable, primaryKey,
  text, timestamp, uuid,
} from 'drizzle-orm/pg-core'
import { localCategory } from './category.js'
import { organization } from './org.js'

/** #12 decision 2: the audience never changes; what a participant can see does. */
export const solicitationStage = pgEnum('solicitation_stage', [
  'draft', 'open', 'evaluating', 'finalists', 'awarded', 'cancelled',
])

/**
 * The six structurally distinct requirement kinds found in real solicitations
 * (#22). Kind routes decisions at evaluation (#15 decision 2) and gates
 * confirmation (#16 decision 1), so a miskind is a correctness bug.
 */
export const requirementKind = pgEnum('requirement_kind', [
  'mandatory_gate',
  'scored_criterion',
  'certification_form',
  'compliance_schedule',
  'pricing',
  'narrative',
])

export const solicitation = pgTable('solicitation', {
  id: uuid('id').primaryKey().defaultRandom(),
  issuerOrgId: uuid('issuer_org_id').notNull().references(() => organization.id),
  /** #31 decision 1 — the retailer's own vocabulary, never the spine. */
  localCategoryId: uuid('local_category_id').notNull().references(() => localCategory.id),
  title: text('title').notNull(),
  stage: solicitationStage('stage').notNull().default('draft'),
  issuedAt: timestamp('issued_at', { withTimezone: true }),
  /** The moment every sealed response opens at once (#14 decision 2). */
  responsesDueAt: timestamp('responses_due_at', { withTimezone: true }),
  /** Per-RFP confidentiality gate (#12 decision 4). Optional by design. */
  acknowledgementRequired: boolean('acknowledgement_required').notNull().default(false),
  /** Coarse band shown pre-acknowledgement; exact quantities are gated (#14 decision 4). */
  volumeBand: text('volume_band'),
}).enableRLS()

/** Legally authoritative. The overlay below is derived from it and may never diverge (#23). */
export const solicitationDocument = pgTable('solicitation_document', {
  id: uuid('id').primaryKey().defaultRandom(),
  solicitationId: uuid('solicitation_id').notNull().references(() => solicitation.id),
  kind: text('kind').notNull(),
  blobRef: text('blob_ref').notNull(),
  sha256: text('sha256').notNull(),
}).enableRLS()

export const confirmationState = pgEnum('confirmation_state', [
  'unconfirmed', 'confirmed', 'corrected',
])

export const requirement = pgTable('requirement', {
  id: uuid('id').primaryKey().defaultRandom(),
  solicitationId: uuid('solicitation_id').notNull().references(() => solicitation.id),
  ordinal: integer('ordinal').notNull(),
  kind: requirementKind('kind').notNull(),
  text: text('text').notNull(),
  responseObligation: text('response_obligation'),

  // Provenance into the authoritative document. Span coverage over these is
  // how the completeness sweep computes what nothing points at (#16 decision 2).
  provenanceDocumentId: uuid('provenance_document_id').references(() => solicitationDocument.id),
  provenancePage: integer('provenance_page'),
  provenanceSpan: jsonb('provenance_span').$type<{ start: number; end: number }>(),

  /**
   * False where satisfaction cannot be established from the response document
   * at all — a fact about submitting rather than about content. Copy counts,
   * delivery method, envelope marking, receipt by a stated hour.
   *
   * Found by the corpus, not by design: Friendship PCS requires "two bid
   * copies". One synthesized bidder asserts it in prose and two do not, and no
   * better extraction would change that. Coverage over such a requirement is
   * neither answered nor not_answered but UNKNOWABLE FROM THE ARTIFACT.
   *
   * Gate evaluation must exclude these rather than fail them — treating the
   * Friendship case as not_answered disqualifies two of three bidders over a
   * packaging detail. The vendor must also never be warned they will fail a
   * gate their document was never able to prove (#16).
   */
  determinableFromResponse: boolean('determinable_from_response').notNull().default(true),

  confirmationState: confirmationState('confirmation_state').notNull().default('unconfirmed'),
  confirmedBy: uuid('confirmed_by'),
  confirmedAt: timestamp('confirmed_at', { withTimezone: true }),
}).enableRLS()

/**
 * The scale is per-document and must never be normalised (#22, #10).
 * The prose anchor is the load-bearing part, not the integer.
 *
 * ── SCORING LEVEL ───────────────────────────────────────────────────────
 *
 *   A score commits at any criterion with NO WEIGHTED CHILDREN.
 *
 * Derived from the rubric, never configured. Two real solicitations forced
 * this rule and neither is special-cased:
 *
 *   Tarrant County   five flat criteria, no nesting — all five are leaves.
 *   Friendship PCS   "Method of Approach" 30 is NOT scoreable; its children
 *                    (15/10/5) are. Likewise "Experience" 20 over 5/5/5/5.
 *
 * A criterion whose sub-items carry NO points stays scoreable at its own
 * level — those sub-items are requirements mapped through
 * `criterion_requirement`, not children. Friendship carries both shapes in
 * one document, which is why the line is drawn on *weight* rather than on
 * nesting.
 *
 * Roll-up is consistent by construction: weighted children sum to their
 * parent's max_points, and corpus/validate-truth.py asserts it.
 * ────────────────────────────────────────────────────────────────────────
 */
export const criterion = pgTable('criterion', {
  id: uuid('id').primaryKey().defaultRandom(),
  solicitationId: uuid('solicitation_id').notNull().references(() => solicitation.id),
  /** Null at the top level. A grouping label that also yields a roll-up total. */
  parentId: uuid('parent_id').references((): AnyPgColumn => criterion.id),
  label: text('label').notNull(),
  maxPoints: numeric('max_points', { precision: 8, scale: 2 }).notNull(),
  weight: numeric('weight', { precision: 8, scale: 2 }).notNull(),
  /** e.g. [{ points: 3, label: 'Average', anchor: 'Meets the stated need…' }] */
  scaleAnchors: jsonb('scale_anchors').$type<
    Array<{ points: number; label: string; anchor: string }>
  >().notNull().default([]),
}).enableRLS()

/**
 * AUTHORED, NOT EXTRACTED — the one object with no span to point at (#33).
 * Frozen at issue; amending it voids affected committed scores (#15 decision 4).
 */
export const criterionRequirement = pgTable('criterion_requirement', {
  criterionId: uuid('criterion_id').notNull().references(() => criterion.id),
  requirementId: uuid('requirement_id').notNull().references(() => requirement.id),
  authoredBy: uuid('authored_by').notNull(),
  frozenAt: timestamp('frozen_at', { withTimezone: true }),
}, (t) => [primaryKey({ columns: [t.criterionId, t.requirementId] })]).enableRLS()

export const priceLine = pgTable('price_line', {
  id: uuid('id').primaryKey().defaultRandom(),
  solicitationId: uuid('solicitation_id').notNull().references(() => solicitation.id),
  label: text('label').notNull(),
  unit: text('unit').notNull(),
})

/** Published with the RFP so price is bindable up front (#10). */
export const evaluationScenario = pgTable('evaluation_scenario', {
  id: uuid('id').primaryKey().defaultRandom(),
  solicitationId: uuid('solicitation_id').notNull().references(() => solicitation.id),
  declaredAt: timestamp('declared_at', { withTimezone: true }).notNull().defaultNow(),
})

/**
 * The exact basket. Sits behind the acknowledgement gate (#14 decision 4) —
 * this is the sensitive half of the scenario, and its own disclosure class.
 */
export const scenarioQuantity = pgTable('scenario_quantity', {
  scenarioId: uuid('scenario_id').notNull().references(() => evaluationScenario.id),
  priceLineId: uuid('price_line_id').notNull().references(() => priceLine.id),
  quantity: numeric('quantity', { precision: 14, scale: 3 }).notNull(),
}, (t) => [
  primaryKey({ columns: [t.scenarioId, t.priceLineId] }),
  pgPolicy('scenario_quantity_read', {
    for: 'select',
    using: sql`class_released(release_level_for_solicitation(
      (SELECT solicitation_id FROM evaluation_scenario WHERE id = scenario_id)
    ), 'scenario_quantity')`,
  }),
]).enableRLS()

export const amendmentClass = pgEnum('amendment_class', ['substantive', 'clarification'])

/**
 * #16 decision 3. The test is whether a bidder who already answered would
 * need to answer differently — not whether the text changed.
 */
export const solicitationAmendment = pgTable('solicitation_amendment', {
  id: uuid('id').primaryKey().defaultRandom(),
  solicitationId: uuid('solicitation_id').notNull().references(() => solicitation.id),
  class: amendmentClass('class').notNull(),
  summary: text('summary').notNull(),
  requirementIds: uuid('requirement_ids').array().notNull().default([]),
  /** Substantive amendments extend the deadline; clarifications do not. */
  deadlineExtendedTo: timestamp('deadline_extended_to', { withTimezone: true }),
  authoredBy: uuid('authored_by').notNull(),
  authoredAt: timestamp('authored_at', { withTimezone: true }).notNull().defaultNow(),
})
