-- Attestor Schema Migration 001: Initial Schema (Compacted)
-- Consolidated from 20251214000001_AddProofChainSchema.sql and 20251216_001_create_rekor_submission_queue.sql
-- for 1.0.0 release
-- Creates the proofchain schema for proof chain persistence and attestor schema for Rekor queue

-- ============================================================================
-- Extensions
-- ============================================================================

CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- ============================================================================
-- Schema Creation
-- ============================================================================

CREATE SCHEMA IF NOT EXISTS proofchain;
CREATE SCHEMA IF NOT EXISTS attestor;

-- ============================================================================
-- Enum Types
-- ============================================================================

DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'verification_result' AND typnamespace = 'proofchain'::regnamespace) THEN
        CREATE TYPE proofchain.verification_result AS ENUM ('pass', 'fail', 'pending');
    END IF;
END $$;

-- ============================================================================
-- ProofChain Schema Tables
-- ============================================================================

-- Trust anchors table (create first - no dependencies)
CREATE TABLE IF NOT EXISTS proofchain.trust_anchors (
    anchor_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    purl_pattern TEXT NOT NULL,
    allowed_keyids TEXT[] NOT NULL,
    allowed_predicate_types TEXT[],
    policy_ref TEXT,
    policy_version TEXT,
    revoked_keys TEXT[] DEFAULT '{}',
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_trust_anchors_pattern ON proofchain.trust_anchors(purl_pattern);
CREATE INDEX IF NOT EXISTS idx_trust_anchors_active ON proofchain.trust_anchors(is_active) WHERE is_active = TRUE;

COMMENT ON TABLE proofchain.trust_anchors IS 'Trust anchor configurations for dependency verification';
COMMENT ON COLUMN proofchain.trust_anchors.purl_pattern IS 'PURL glob pattern (e.g., pkg:npm/*)';
COMMENT ON COLUMN proofchain.trust_anchors.revoked_keys IS 'Key IDs that have been revoked but may appear in old proofs';

-- SBOM entries table
CREATE TABLE IF NOT EXISTS proofchain.sbom_entries (
    entry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    bom_digest VARCHAR(64) NOT NULL,
    purl TEXT NOT NULL,
    version TEXT,
    artifact_digest VARCHAR(64),
    trust_anchor_id UUID REFERENCES proofchain.trust_anchors(anchor_id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_sbom_entry UNIQUE (bom_digest, purl, version)
);

CREATE INDEX IF NOT EXISTS idx_sbom_entries_bom_digest ON proofchain.sbom_entries(bom_digest);
CREATE INDEX IF NOT EXISTS idx_sbom_entries_purl ON proofchain.sbom_entries(purl);
CREATE INDEX IF NOT EXISTS idx_sbom_entries_artifact ON proofchain.sbom_entries(artifact_digest);
CREATE INDEX IF NOT EXISTS idx_sbom_entries_anchor ON proofchain.sbom_entries(trust_anchor_id);

COMMENT ON TABLE proofchain.sbom_entries IS 'SBOM component entries with content-addressed identifiers';
COMMENT ON COLUMN proofchain.sbom_entries.bom_digest IS 'SHA-256 hash of the parent SBOM document';
COMMENT ON COLUMN proofchain.sbom_entries.purl IS 'Package URL (PURL) of the component';
COMMENT ON COLUMN proofchain.sbom_entries.artifact_digest IS 'SHA-256 hash of the component artifact if available';

-- DSSE envelopes table
CREATE TABLE IF NOT EXISTS proofchain.dsse_envelopes (
    env_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    entry_id UUID NOT NULL REFERENCES proofchain.sbom_entries(entry_id) ON DELETE CASCADE,
    predicate_type TEXT NOT NULL,
    signer_keyid TEXT NOT NULL,
    body_hash VARCHAR(64) NOT NULL,
    envelope_blob_ref TEXT NOT NULL,
    signed_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_dsse_envelope UNIQUE (entry_id, predicate_type, body_hash)
);

CREATE INDEX IF NOT EXISTS idx_dsse_entry_predicate ON proofchain.dsse_envelopes(entry_id, predicate_type);
CREATE INDEX IF NOT EXISTS idx_dsse_signer ON proofchain.dsse_envelopes(signer_keyid);
CREATE INDEX IF NOT EXISTS idx_dsse_body_hash ON proofchain.dsse_envelopes(body_hash);

COMMENT ON TABLE proofchain.dsse_envelopes IS 'Signed DSSE envelopes for proof chain statements';
COMMENT ON COLUMN proofchain.dsse_envelopes.predicate_type IS 'Predicate type URI (e.g., evidence.stella/v1)';
COMMENT ON COLUMN proofchain.dsse_envelopes.envelope_blob_ref IS 'Reference to blob storage (OCI, S3, file)';

-- Spines table
CREATE TABLE IF NOT EXISTS proofchain.spines (
    entry_id UUID PRIMARY KEY REFERENCES proofchain.sbom_entries(entry_id) ON DELETE CASCADE,
    bundle_id VARCHAR(64) NOT NULL,
    evidence_ids TEXT[] NOT NULL,
    reasoning_id VARCHAR(64) NOT NULL,
    vex_id VARCHAR(64) NOT NULL,
    anchor_id UUID REFERENCES proofchain.trust_anchors(anchor_id) ON DELETE SET NULL,
    policy_version TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_spine_bundle UNIQUE (bundle_id)
);

CREATE INDEX IF NOT EXISTS idx_spines_bundle ON proofchain.spines(bundle_id);
CREATE INDEX IF NOT EXISTS idx_spines_anchor ON proofchain.spines(anchor_id);
CREATE INDEX IF NOT EXISTS idx_spines_policy ON proofchain.spines(policy_version);

COMMENT ON TABLE proofchain.spines IS 'Proof spines linking evidence to verdicts via merkle aggregation';
COMMENT ON COLUMN proofchain.spines.bundle_id IS 'ProofBundleID (merkle root of all components)';
COMMENT ON COLUMN proofchain.spines.evidence_ids IS 'Array of EvidenceIDs in sorted order';

-- Rekor entries table
CREATE TABLE IF NOT EXISTS proofchain.rekor_entries (
    dsse_sha256 VARCHAR(64) PRIMARY KEY,
    log_index BIGINT NOT NULL,
    log_id TEXT NOT NULL,
    uuid TEXT NOT NULL,
    integrated_time BIGINT NOT NULL,
    inclusion_proof JSONB NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    env_id UUID REFERENCES proofchain.dsse_envelopes(env_id) ON DELETE SET NULL
);

CREATE INDEX IF NOT EXISTS idx_rekor_log_index ON proofchain.rekor_entries(log_index);
CREATE INDEX IF NOT EXISTS idx_rekor_log_id ON proofchain.rekor_entries(log_id);
CREATE INDEX IF NOT EXISTS idx_rekor_uuid ON proofchain.rekor_entries(uuid);
CREATE INDEX IF NOT EXISTS idx_rekor_env ON proofchain.rekor_entries(env_id);

COMMENT ON TABLE proofchain.rekor_entries IS 'Rekor transparency log entries for verification';
COMMENT ON COLUMN proofchain.rekor_entries.inclusion_proof IS 'Merkle inclusion proof from Rekor';

-- Audit log table
CREATE TABLE IF NOT EXISTS proofchain.audit_log (
    log_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    operation TEXT NOT NULL,
    entity_type TEXT NOT NULL,
    entity_id TEXT NOT NULL,
    actor TEXT,
    details JSONB,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_audit_entity ON proofchain.audit_log(entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_created ON proofchain.audit_log(created_at DESC);

COMMENT ON TABLE proofchain.audit_log IS 'Audit log for proof chain operations';

-- ============================================================================
-- Attestor Schema Tables
-- ============================================================================

-- Rekor submission queue table
CREATE TABLE IF NOT EXISTS attestor.rekor_submission_queue (
    id UUID PRIMARY KEY,
    tenant_id TEXT NOT NULL,
    bundle_sha256 TEXT NOT NULL,
    dsse_payload BYTEA NOT NULL,
    backend TEXT NOT NULL DEFAULT 'primary',

    -- Status lifecycle: pending -> submitting -> submitted | retrying -> dead_letter
    status TEXT NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'submitting', 'retrying', 'submitted', 'dead_letter')),

    attempt_count INTEGER NOT NULL DEFAULT 0,
    max_attempts INTEGER NOT NULL DEFAULT 5,
    next_retry_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Populated on success
    rekor_uuid TEXT,
    rekor_index BIGINT,

    -- Populated on failure
    last_error TEXT
);

COMMENT ON TABLE attestor.rekor_submission_queue IS
    'Durable retry queue for Rekor transparency log submissions';
COMMENT ON COLUMN attestor.rekor_submission_queue.status IS
    'Submission lifecycle: pending -> submitting -> (submitted | retrying -> dead_letter)';
COMMENT ON COLUMN attestor.rekor_submission_queue.backend IS
    'Target Rekor backend (primary or mirror)';
COMMENT ON COLUMN attestor.rekor_submission_queue.dsse_payload IS
    'Serialized DSSE envelope to submit';

-- Index for dequeue operations (status + next_retry_at for SKIP LOCKED queries)
CREATE INDEX IF NOT EXISTS idx_rekor_queue_dequeue
    ON attestor.rekor_submission_queue (status, next_retry_at)
    WHERE status IN ('pending', 'retrying');

-- Index for tenant-scoped queries
CREATE INDEX IF NOT EXISTS idx_rekor_queue_tenant
    ON attestor.rekor_submission_queue (tenant_id);

-- Index for bundle lookup (deduplication check)
CREATE INDEX IF NOT EXISTS idx_rekor_queue_bundle
    ON attestor.rekor_submission_queue (tenant_id, bundle_sha256);

-- Index for dead letter management
CREATE INDEX IF NOT EXISTS idx_rekor_queue_dead_letter
    ON attestor.rekor_submission_queue (status, updated_at)
    WHERE status = 'dead_letter';

-- Index for cleanup of completed submissions
CREATE INDEX IF NOT EXISTS idx_rekor_queue_completed
    ON attestor.rekor_submission_queue (status, updated_at)
    WHERE status = 'submitted';

-- ============================================================================
-- Trigger Functions
-- ============================================================================

CREATE OR REPLACE FUNCTION proofchain.update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Apply updated_at trigger to trust_anchors
DROP TRIGGER IF EXISTS update_trust_anchors_updated_at ON proofchain.trust_anchors;
CREATE TRIGGER update_trust_anchors_updated_at
    BEFORE UPDATE ON proofchain.trust_anchors
    FOR EACH ROW
    EXECUTE FUNCTION proofchain.update_updated_at_column();

-- Apply updated_at trigger to rekor_submission_queue
DROP TRIGGER IF EXISTS update_rekor_queue_updated_at ON attestor.rekor_submission_queue;
CREATE TRIGGER update_rekor_queue_updated_at
    BEFORE UPDATE ON attestor.rekor_submission_queue
    FOR EACH ROW
    EXECUTE FUNCTION proofchain.update_updated_at_column();
