ALTER TABLE article_versions
    ADD COLUMN IF NOT EXISTS plaintext_snapshot TEXT,
    ADD COLUMN IF NOT EXISTS manual_review_status VARCHAR(16) NOT NULL DEFAULT 'none',
    ADD COLUMN IF NOT EXISTS manual_review_id BIGINT,
    ADD COLUMN IF NOT EXISTS manual_review_updated_at TIMESTAMPTZ;

ALTER TABLE article_versions
    DROP CONSTRAINT IF EXISTS ck_article_versions_manual_review_status;

ALTER TABLE article_versions
    ADD CONSTRAINT ck_article_versions_manual_review_status
    CHECK (manual_review_status IN ('none', 'pending', 'approved', 'rejected'));

COMMENT ON COLUMN article_versions.plaintext_snapshot IS
    'Plaintext extracted at save time, used by compliance gate to skip diff reconstruction.';

COMMENT ON COLUMN article_versions.manual_review_status IS
    'Hot-path manual review stamp: none|pending|approved|rejected.';

CREATE TABLE IF NOT EXISTS compliance_check_records (
    id                 BIGSERIAL PRIMARY KEY,
    tenant_id          BIGINT NOT NULL REFERENCES tenants(id),
    article_id         BIGINT REFERENCES articles(id) ON DELETE SET NULL,
    article_version_id BIGINT REFERENCES article_versions(id) ON DELETE SET NULL,
    target_platforms   TEXT[] NOT NULL DEFAULT '{}',
    content_hash       CHAR(64) NOT NULL,
    policy_fingerprint VARCHAR(64) NOT NULL,
    trigger_source     VARCHAR(32) NOT NULL,
    enforcement_mode   VARCHAR(24) NOT NULL,
    highest_level      VARCHAR(16),
    passed             BOOLEAN NOT NULL,
    hit_count          INT NOT NULL DEFAULT 0,
    stored_hit_count   INT NOT NULL DEFAULT 0,
    truncated          BOOLEAN NOT NULL DEFAULT FALSE,
    dict_versions      JSONB NOT NULL DEFAULT '{}'::jsonb,
    llm_used           BOOLEAN NOT NULL DEFAULT FALSE,
    duration_ms        INT NOT NULL DEFAULT 0,
    checked_by         BIGINT,
    checked_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    manual_review_status VARCHAR(16) NOT NULL DEFAULT 'none',
    manual_review_id   BIGINT,
    CONSTRAINT ck_compliance_check_trigger_source CHECK (
        trigger_source IN (
            'editor_manual',
            'publish_gate',
            'auto_draft',
            'api',
            'scheduler_recheck',
            'manual_review_bypass'
        )
    ),
    CONSTRAINT ck_compliance_check_mode CHECK (enforcement_mode IN ('mandatory', 'advisory', 'disabled')),
    CONSTRAINT ck_compliance_check_highest_level CHECK (highest_level IS NULL OR highest_level IN ('block', 'high', 'medium', 'info')),
    CONSTRAINT ck_compliance_check_manual_status CHECK (manual_review_status IN ('none', 'pending', 'approved', 'rejected'))
);

CREATE INDEX IF NOT EXISTS idx_compliance_check_records_article_checked
    ON compliance_check_records (tenant_id, article_id, checked_at DESC);

CREATE INDEX IF NOT EXISTS idx_compliance_check_records_version_checked
    ON compliance_check_records (article_version_id, checked_at DESC)
    WHERE article_version_id IS NOT NULL;

CREATE TABLE IF NOT EXISTS compliance_violations (
    id                         BIGSERIAL PRIMARY KEY,
    record_id                  BIGINT NOT NULL REFERENCES compliance_check_records(id) ON DELETE CASCADE,
    tenant_id                  BIGINT NOT NULL REFERENCES tenants(id),
    platform_code              TEXT,
    source                     VARCHAR(24) NOT NULL,
    dictionary_code            VARCHAR(64),
    dictionary_name            VARCHAR(128),
    term_pattern               TEXT,
    matched_text               TEXT NOT NULL,
    start_offset               INT NOT NULL DEFAULT 0,
    end_offset                 INT NOT NULL DEFAULT 0,
    dom_path                   TEXT,
    level                      VARCHAR(16) NOT NULL,
    hint                       TEXT,
    reference_law              TEXT,
    acknowledged_in_ack_id     BIGINT,
    created_at                 TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT ck_compliance_violation_source CHECK (source IN ('dict', 'regex', 'llm')),
    CONSTRAINT ck_compliance_violation_level CHECK (level IN ('block', 'high', 'medium', 'info')),
    CONSTRAINT ck_compliance_violation_offsets CHECK (start_offset >= 0 AND end_offset >= start_offset),
    CONSTRAINT ck_violations_matched_text_len CHECK (char_length(matched_text) <= 80)
);

CREATE INDEX IF NOT EXISTS idx_compliance_violations_record
    ON compliance_violations (record_id, id);

CREATE INDEX IF NOT EXISTS idx_compliance_violations_tenant_level
    ON compliance_violations (tenant_id, level, id DESC);

CREATE TABLE IF NOT EXISTS compliance_ack_records (
    id                 BIGSERIAL PRIMARY KEY,
    tenant_id          BIGINT NOT NULL REFERENCES tenants(id),
    article_id         BIGINT NOT NULL REFERENCES articles(id),
    article_version_id BIGINT NOT NULL REFERENCES article_versions(id),
    check_record_id    BIGINT NOT NULL REFERENCES compliance_check_records(id) ON DELETE CASCADE,
    content_hash       CHAR(64) NOT NULL,
    policy_fingerprint VARCHAR(64) NOT NULL,
    acknowledged_by    BIGINT NOT NULL REFERENCES users(id),
    ack_violation_ids  BIGINT[] NOT NULL,
    expires_at         TIMESTAMPTZ NOT NULL,
    consumed_at        TIMESTAMPTZ,
    consumed_by_job_id UUID,
    created_at         TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_ack_lookup
    ON compliance_ack_records (tenant_id, article_id, article_version_id, check_record_id)
    WHERE consumed_at IS NULL;

ALTER TABLE compliance_violations
    DROP CONSTRAINT IF EXISTS fk_compliance_violations_ack;

ALTER TABLE compliance_violations
    ADD CONSTRAINT fk_compliance_violations_ack
    FOREIGN KEY (acknowledged_in_ack_id)
    REFERENCES compliance_ack_records(id)
    ON DELETE SET NULL;

CREATE TABLE IF NOT EXISTS compliance_review_jobs (
    id                 BIGSERIAL PRIMARY KEY,
    message_id         UUID NOT NULL,
    tenant_id          BIGINT NOT NULL REFERENCES tenants(id),
    article_id         BIGINT NOT NULL REFERENCES articles(id),
    article_version_id BIGINT NOT NULL REFERENCES article_versions(id),
    check_record_id    BIGINT NOT NULL REFERENCES compliance_check_records(id) ON DELETE CASCADE,
    status             VARCHAR(16) NOT NULL DEFAULT 'pending',
    attempts           INT NOT NULL DEFAULT 0,
    available_at       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    locked_at          TIMESTAMPTZ,
    locked_by          TEXT,
    last_error         TEXT,
    created_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT uk_compliance_review_jobs_record UNIQUE (check_record_id),
    CONSTRAINT uk_compliance_review_jobs_message UNIQUE (message_id),
    CONSTRAINT ck_compliance_review_job_status CHECK (status IN ('pending', 'running', 'done', 'failed'))
);

CREATE INDEX IF NOT EXISTS idx_compliance_review_jobs_pending
    ON compliance_review_jobs (available_at, id)
    WHERE status = 'pending';

CREATE TABLE IF NOT EXISTS compliance_manual_reviews (
    id                          BIGSERIAL PRIMARY KEY,
    tenant_id                   BIGINT NOT NULL REFERENCES tenants(id),
    article_id                  BIGINT NOT NULL REFERENCES articles(id),
    article_version_id          BIGINT NOT NULL REFERENCES article_versions(id),
    original_check_record_id    BIGINT REFERENCES compliance_check_records(id) ON DELETE SET NULL,
    original_content_hash       CHAR(64) NOT NULL,
    original_policy_fingerprint VARCHAR(64) NOT NULL,
    original_target_platforms   TEXT[] NOT NULL DEFAULT '{}',
    original_violation_summary  JSONB NOT NULL DEFAULT '[]'::jsonb,
    requested_by                BIGINT NOT NULL REFERENCES users(id),
    requested_at                TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    request_note                TEXT,
    status                      VARCHAR(16) NOT NULL DEFAULT 'pending',
    cancelled_by                BIGINT REFERENCES users(id),
    cancelled_at                TIMESTAMPTZ,
    cancel_reason               TEXT,
    reviewed_by                 BIGINT,
    reviewed_at                 TIMESTAMPTZ,
    decision_reason             TEXT,
    CONSTRAINT ck_manual_review_request_note_len CHECK (request_note IS NULL OR char_length(request_note) <= 500),
    CONSTRAINT ck_manual_review_cancel_reason_len CHECK (cancel_reason IS NULL OR char_length(cancel_reason) <= 500),
    CONSTRAINT ck_manual_review_decision_reason_len CHECK (decision_reason IS NULL OR char_length(decision_reason) <= 1000),
    CONSTRAINT ck_manual_review_status CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled')),
    CONSTRAINT ck_manual_review_decision CHECK (
        (status = 'pending' AND reviewed_by IS NULL AND reviewed_at IS NULL AND decision_reason IS NULL
                             AND cancelled_by IS NULL AND cancelled_at IS NULL AND cancel_reason IS NULL) OR
        (status IN ('approved','rejected') AND reviewed_by IS NOT NULL AND reviewed_at IS NOT NULL AND decision_reason IS NOT NULL
                                          AND cancelled_by IS NULL AND cancelled_at IS NULL AND cancel_reason IS NULL) OR
        (status = 'cancelled' AND reviewed_by IS NULL AND reviewed_at IS NULL AND decision_reason IS NULL
                              AND cancelled_by IS NOT NULL AND cancelled_at IS NOT NULL)
    )
);

CREATE UNIQUE INDEX IF NOT EXISTS uk_manual_review_active_per_version
    ON compliance_manual_reviews (article_version_id)
    WHERE status IN ('pending', 'approved', 'rejected');

CREATE INDEX IF NOT EXISTS idx_manual_review_pending_queue
    ON compliance_manual_reviews (requested_at)
    WHERE status = 'pending';

CREATE INDEX IF NOT EXISTS idx_manual_review_lookup_by_version
    ON compliance_manual_reviews (article_version_id, status);

CREATE INDEX IF NOT EXISTS idx_manual_review_tenant_status
    ON compliance_manual_reviews (tenant_id, status, requested_at DESC);

ALTER TABLE article_versions
    DROP CONSTRAINT IF EXISTS fk_article_versions_manual_review;

ALTER TABLE article_versions
    ADD CONSTRAINT fk_article_versions_manual_review
    FOREIGN KEY (manual_review_id)
    REFERENCES compliance_manual_reviews(id)
    ON DELETE SET NULL;

ALTER TABLE desktop_publish_jobs
    ADD COLUMN IF NOT EXISTS article_id BIGINT REFERENCES articles(id),
    ADD COLUMN IF NOT EXISTS article_version_id BIGINT REFERENCES article_versions(id),
    ADD COLUMN IF NOT EXISTS status VARCHAR(32) NOT NULL DEFAULT 'queued',
    ADD COLUMN IF NOT EXISTS compliance_blocked_record_id BIGINT REFERENCES compliance_check_records(id) ON DELETE SET NULL,
    ADD COLUMN IF NOT EXISTS compliance_blocked_at TIMESTAMPTZ,
    ADD COLUMN IF NOT EXISTS compliance_blocked_reason TEXT;

CREATE INDEX IF NOT EXISTS idx_desktop_publish_jobs_status_scheduled
    ON desktop_publish_jobs (status, scheduled_at, created_at)
    WHERE status IN ('queued', 'blocked_by_compliance');
