BEGIN;

CREATE TABLE IF NOT EXISTS schema_migrations (
    version text PRIMARY KEY,
    applied_at timestamptz NOT NULL DEFAULT now()
);

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE OR REPLACE FUNCTION touch_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    NEW.updated_at = now();
    RETURN NEW;
END;
$$;

CREATE TABLE media_assets (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    kind text NOT NULL CHECK (kind IN (
        'channel_logo', 'video_thumbnail', 'interviewee_professional',
        'interviewee_personal', 'origin_icon', 'other'
    )),
    source_url text,
    storage_path text NOT NULL,
    sha256 text NOT NULL,
    mime_type text NOT NULL,
    size_bytes bigint NOT NULL CHECK (size_bytes >= 0),
    width integer CHECK (width IS NULL OR width > 0),
    height integer CHECK (height IS NULL OR height > 0),
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT media_assets_sha256_not_blank CHECK (length(trim(sha256)) > 0),
    CONSTRAINT media_assets_storage_path_not_blank CHECK (length(trim(storage_path)) > 0),
    CONSTRAINT media_assets_sha256_unique UNIQUE (sha256)
);

CREATE TABLE podcast_channels (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    youtube_channel_id text NOT NULL,
    name text NOT NULL,
    canonical_url text NOT NULL,
    logo_asset_id uuid REFERENCES media_assets(id) ON DELETE SET NULL,
    status text NOT NULL DEFAULT 'candidate'
        CHECK (status IN ('candidate', 'selected', 'active', 'removed', 'failed')),
    metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
    discovered_at timestamptz NOT NULL DEFAULT now(),
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    deleted_at timestamptz,
    CONSTRAINT podcast_channels_youtube_id_not_blank CHECK (length(trim(youtube_channel_id)) > 0),
    CONSTRAINT podcast_channels_name_not_blank CHECK (length(trim(name)) > 0),
    CONSTRAINT podcast_channels_url_not_blank CHECK (length(trim(canonical_url)) > 0),
    CONSTRAINT podcast_channels_youtube_id_unique UNIQUE (youtube_channel_id),
    CONSTRAINT podcast_channels_canonical_url_unique UNIQUE (canonical_url)
);

CREATE TABLE videos (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    channel_id uuid NOT NULL REFERENCES podcast_channels(id) ON DELETE CASCADE,
    youtube_video_id text NOT NULL,
    canonical_url text NOT NULL,
    title text NOT NULL,
    description text NOT NULL DEFAULT '',
    published_at timestamptz,
    duration_seconds integer CHECK (duration_seconds IS NULL OR duration_seconds >= 0),
    thumbnail_asset_id uuid REFERENCES media_assets(id) ON DELETE SET NULL,
    processing_status text NOT NULL DEFAULT 'pending'
        CHECK (processing_status IN ('pending', 'processing', 'processed', 'skipped', 'failed')),
    metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    deleted_at timestamptz,
    CONSTRAINT videos_youtube_id_not_blank CHECK (length(trim(youtube_video_id)) > 0),
    CONSTRAINT videos_title_not_blank CHECK (length(trim(title)) > 0),
    CONSTRAINT videos_youtube_id_unique UNIQUE (youtube_video_id),
    CONSTRAINT videos_canonical_url_unique UNIQUE (canonical_url)
);

CREATE TABLE transcripts (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    video_id uuid NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
    language text NOT NULL,
    source text NOT NULL CHECK (source IN ('youtube', 'translated', 'speech_to_text', 'manual')),
    text_content text NOT NULL DEFAULT '',
    content_hash text NOT NULL,
    status text NOT NULL DEFAULT 'ready'
        CHECK (status IN ('pending', 'ready', 'unavailable', 'failed')),
    is_generated boolean NOT NULL DEFAULT false,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT transcripts_language_not_blank CHECK (length(trim(language)) > 0),
    CONSTRAINT transcripts_hash_not_blank CHECK (length(trim(content_hash)) > 0),
    CONSTRAINT transcripts_video_language_source_unique UNIQUE (video_id, language, source)
);

CREATE TABLE categories (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    display_name text NOT NULL,
    normalized_name text NOT NULL,
    description text NOT NULL DEFAULT '',
    created_by text NOT NULL DEFAULT 'ai' CHECK (created_by IN ('ai', 'manual', 'system')),
    active boolean NOT NULL DEFAULT true,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT categories_display_name_not_blank CHECK (length(trim(display_name)) > 0),
    CONSTRAINT categories_normalized_name_not_blank CHECK (length(trim(normalized_name)) > 0),
    CONSTRAINT categories_normalized_name_unique UNIQUE (normalized_name)
);

CREATE TABLE pipeline_runs (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    kind text NOT NULL CHECK (kind IN (
        'podcast_discovery', 'interviewee_extraction', 'contact_extraction'
    )),
    mode text NOT NULL DEFAULT 'manual' CHECK (mode IN ('manual', 'automatic')),
    status text NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'running', 'paused', 'cancelling', 'cancelled', 'completed', 'failed')),
    idempotency_key text,
    requested_by uuid,
    input jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(input) = 'object'),
    progress_current bigint NOT NULL DEFAULT 0 CHECK (progress_current >= 0),
    progress_total bigint CHECK (progress_total IS NULL OR progress_total >= 0),
    stats jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(stats) = 'object'),
    error_code text,
    error_message text,
    started_at timestamptz,
    finished_at timestamptz,
    cancel_requested_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT pipeline_runs_idempotency_key_unique UNIQUE (idempotency_key),
    CONSTRAINT pipeline_runs_progress_valid CHECK (
        progress_total IS NULL OR progress_current <= progress_total
    )
);

CREATE TABLE interviewees (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    primary_category_id uuid REFERENCES categories(id) ON DELETE RESTRICT,
    display_name text NOT NULL,
    real_name text,
    brand_name text,
    normalized_display_name text NOT NULL,
    normalized_real_name text,
    normalized_brand_name text,
    professional_summary text NOT NULL DEFAULT '',
    public_bio text NOT NULL DEFAULT '',
    creator_content_type text,
    creator_audience text,
    professional_image_asset_id uuid REFERENCES media_assets(id) ON DELETE SET NULL,
    personal_image_asset_id uuid REFERENCES media_assets(id) ON DELETE SET NULL,
    status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'merged', 'archived')),
    dedup_review_status text NOT NULL DEFAULT 'unreviewed'
        CHECK (dedup_review_status IN ('unreviewed', 'confirmed', 'needs_review')),
    created_in_run_id uuid REFERENCES pipeline_runs(id) ON DELETE SET NULL,
    metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    deleted_at timestamptz,
    CONSTRAINT interviewees_display_name_not_blank CHECK (length(trim(display_name)) > 0),
    CONSTRAINT interviewees_normalized_name_not_blank CHECK (length(trim(normalized_display_name)) > 0)
);

CREATE TABLE interviewee_aliases (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    interviewee_id uuid NOT NULL REFERENCES interviewees(id) ON DELETE CASCADE,
    alias text NOT NULL,
    normalized_alias text NOT NULL,
    kind text NOT NULL DEFAULT 'other'
        CHECK (kind IN ('real_name', 'brand_name', 'stage_name', 'social_handle', 'other')),
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT interviewee_aliases_alias_not_blank CHECK (length(trim(alias)) > 0),
    CONSTRAINT interviewee_aliases_normalized_not_blank CHECK (length(trim(normalized_alias)) > 0),
    CONSTRAINT interviewee_aliases_unique UNIQUE (interviewee_id, kind, normalized_alias)
);

CREATE TABLE interviewee_redirects (
    old_interviewee_id uuid PRIMARY KEY REFERENCES interviewees(id) ON DELETE CASCADE,
    canonical_interviewee_id uuid NOT NULL REFERENCES interviewees(id) ON DELETE RESTRICT,
    reason text NOT NULL DEFAULT '',
    merged_by text NOT NULL DEFAULT 'manual' CHECK (merged_by IN ('manual', 'system')),
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT interviewee_redirects_not_self CHECK (old_interviewee_id <> canonical_interviewee_id)
);

CREATE TABLE appearances (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    interviewee_id uuid NOT NULL REFERENCES interviewees(id) ON DELETE CASCADE,
    video_id uuid NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
    confidence real NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
    evidence text NOT NULL DEFAULT '',
    evidence_hash text,
    extraction_source text NOT NULL DEFAULT 'ai' CHECK (extraction_source IN ('ai', 'manual', 'metadata')),
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT appearances_interviewee_video_unique UNIQUE (interviewee_id, video_id)
);

CREATE TABLE origins (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    display_name text NOT NULL,
    canonical_url text NOT NULL,
    domain text NOT NULL,
    source_type text NOT NULL DEFAULT 'website'
        CHECK (source_type IN ('youtube', 'instagram', 'linktree', 'website', 'google', 'other')),
    icon_asset_id uuid REFERENCES media_assets(id) ON DELETE SET NULL,
    first_seen_at timestamptz NOT NULL DEFAULT now(),
    last_seen_at timestamptz NOT NULL DEFAULT now(),
    metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT origins_name_not_blank CHECK (length(trim(display_name)) > 0),
    CONSTRAINT origins_url_not_blank CHECK (length(trim(canonical_url)) > 0),
    CONSTRAINT origins_domain_not_blank CHECK (length(trim(domain)) > 0),
    CONSTRAINT origins_canonical_url_unique UNIQUE (canonical_url)
);

CREATE TABLE contacts (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    interviewee_id uuid NOT NULL REFERENCES interviewees(id) ON DELETE CASCADE,
    primary_origin_id uuid NOT NULL REFERENCES origins(id) ON DELETE RESTRICT,
    contact_type text NOT NULL
        CHECK (contact_type IN (
            'email', 'phone', 'whatsapp', 'instagram', 'linkedin', 'facebook',
            'tiktok', 'x', 'telegram', 'youtube', 'website', 'other'
        )),
    raw_value text NOT NULL,
    normalized_value text NOT NULL,
    relationship_kind text NOT NULL CHECK (relationship_kind IN ('personal', 'commercial')),
    label text,
    confidence real NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
    status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'stale', 'suppressed', 'deleted')),
    discovered_in_run_id uuid REFERENCES pipeline_runs(id) ON DELETE SET NULL,
    first_seen_at timestamptz NOT NULL DEFAULT now(),
    last_seen_at timestamptz NOT NULL DEFAULT now(),
    last_verified_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    deleted_at timestamptz,
    CONSTRAINT contacts_raw_value_not_blank CHECK (length(trim(raw_value)) > 0),
    CONSTRAINT contacts_normalized_value_not_blank CHECK (length(trim(normalized_value)) > 0)
);

CREATE UNIQUE INDEX contacts_active_identity_unique
    ON contacts (interviewee_id, contact_type, normalized_value)
    WHERE deleted_at IS NULL AND status <> 'deleted';

CREATE TABLE contact_evidence (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    contact_id uuid NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
    origin_id uuid NOT NULL REFERENCES origins(id) ON DELETE RESTRICT,
    page_url text NOT NULL,
    evidence_text text NOT NULL DEFAULT '',
    evidence_hash text,
    confidence real NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
    collected_at timestamptz NOT NULL DEFAULT now(),
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX contact_evidence_unique
    ON contact_evidence (contact_id, origin_id, page_url, COALESCE(evidence_hash, ''));

CREATE TABLE jobs (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id uuid REFERENCES pipeline_runs(id) ON DELETE CASCADE,
    parent_job_id uuid REFERENCES jobs(id) ON DELETE SET NULL,
    kind text NOT NULL,
    payload jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(payload) = 'object'),
    result jsonb CHECK (result IS NULL OR jsonb_typeof(result) = 'object'),
    status text NOT NULL DEFAULT 'queued'
        CHECK (status IN ('queued', 'running', 'retry_scheduled', 'succeeded', 'failed', 'cancelled')),
    priority integer NOT NULL DEFAULT 0,
    idempotency_key text,
    attempt_count integer NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
    max_attempts integer NOT NULL DEFAULT 5 CHECK (max_attempts > 0),
    available_at timestamptz NOT NULL DEFAULT now(),
    locked_at timestamptz,
    locked_by text,
    heartbeat_at timestamptz,
    started_at timestamptz,
    finished_at timestamptz,
    cancel_requested_at timestamptz,
    last_error_code text,
    last_error_message text,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT jobs_kind_not_blank CHECK (length(trim(kind)) > 0),
    CONSTRAINT jobs_idempotency_key_unique UNIQUE (idempotency_key),
    CONSTRAINT jobs_attempt_bounds CHECK (attempt_count <= max_attempts)
);

CREATE TABLE job_attempts (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    job_id uuid NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
    attempt_no integer NOT NULL CHECK (attempt_no > 0),
    worker_id text NOT NULL,
    status text NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'succeeded', 'failed', 'cancelled')),
    error_code text,
    error_message text,
    started_at timestamptz NOT NULL DEFAULT now(),
    finished_at timestamptz,
    metrics jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metrics) = 'object'),
    CONSTRAINT job_attempts_job_attempt_unique UNIQUE (job_id, attempt_no)
);

CREATE TABLE crawl_pages (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id uuid NOT NULL REFERENCES pipeline_runs(id) ON DELETE CASCADE,
    interviewee_id uuid NOT NULL REFERENCES interviewees(id) ON DELETE CASCADE,
    origin_id uuid REFERENCES origins(id) ON DELETE SET NULL,
    canonical_url text NOT NULL,
    depth smallint NOT NULL CHECK (depth >= 0 AND depth <= 5),
    status text NOT NULL DEFAULT 'queued'
        CHECK (status IN ('queued', 'fetching', 'fetched', 'skipped', 'failed', 'blocked')),
    relevance real CHECK (relevance IS NULL OR (relevance >= 0 AND relevance <= 1)),
    http_status integer CHECK (http_status IS NULL OR (http_status >= 100 AND http_status <= 599)),
    content_hash text,
    title text,
    extracted_text text,
    content_bytes bigint CHECK (content_bytes IS NULL OR content_bytes >= 0),
    error_code text,
    error_message text,
    queued_at timestamptz NOT NULL DEFAULT now(),
    fetched_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT crawl_pages_url_not_blank CHECK (length(trim(canonical_url)) > 0),
    CONSTRAINT crawl_pages_run_url_unique UNIQUE (run_id, canonical_url)
);

CREATE TABLE crawl_edges (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id uuid NOT NULL REFERENCES pipeline_runs(id) ON DELETE CASCADE,
    from_page_id uuid REFERENCES crawl_pages(id) ON DELETE CASCADE,
    to_page_id uuid REFERENCES crawl_pages(id) ON DELETE SET NULL,
    discovered_url text NOT NULL,
    anchor_text text,
    relationship text NOT NULL DEFAULT 'link' CHECK (relationship IN ('seed', 'link', 'redirect', 'canonical')),
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT crawl_edges_url_not_blank CHECK (length(trim(discovered_url)) > 0)
);

CREATE UNIQUE INDEX crawl_edges_unique
    ON crawl_edges (run_id, COALESCE(from_page_id, '00000000-0000-0000-0000-000000000000'::uuid), discovered_url, relationship);

CREATE TABLE interviewee_candidates (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id uuid NOT NULL REFERENCES pipeline_runs(id) ON DELETE CASCADE,
    video_id uuid NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
    proposed_name text NOT NULL,
    normalized_name text NOT NULL,
    proposed_real_name text,
    proposed_brand_name text,
    professional_summary text NOT NULL DEFAULT '',
    evidence text NOT NULL DEFAULT '',
    evidence_hash text,
    confidence real NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
    status text NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'matched', 'created', 'rejected')),
    matched_interviewee_id uuid REFERENCES interviewees(id) ON DELETE SET NULL,
    ai_call_id uuid,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT interviewee_candidates_name_not_blank CHECK (length(trim(proposed_name)) > 0),
    CONSTRAINT interviewee_candidates_normalized_not_blank CHECK (length(trim(normalized_name)) > 0)
);

CREATE UNIQUE INDEX interviewee_candidates_unique
    ON interviewee_candidates (run_id, video_id, normalized_name, COALESCE(evidence_hash, ''));

CREATE TABLE contact_candidates (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id uuid NOT NULL REFERENCES pipeline_runs(id) ON DELETE CASCADE,
    interviewee_id uuid NOT NULL REFERENCES interviewees(id) ON DELETE CASCADE,
    origin_id uuid NOT NULL REFERENCES origins(id) ON DELETE CASCADE,
    crawl_page_id uuid REFERENCES crawl_pages(id) ON DELETE SET NULL,
    contact_type text NOT NULL
        CHECK (contact_type IN (
            'email', 'phone', 'whatsapp', 'instagram', 'linkedin', 'facebook',
            'tiktok', 'x', 'telegram', 'youtube', 'website', 'other'
        )),
    raw_value text NOT NULL,
    normalized_value text NOT NULL,
    proposed_relationship_kind text CHECK (proposed_relationship_kind IN ('personal', 'commercial')),
    proposed_label text,
    evidence text NOT NULL DEFAULT '',
    confidence real NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
    status text NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'accepted', 'rejected', 'needs_review')),
    rejection_reason text,
    accepted_contact_id uuid REFERENCES contacts(id) ON DELETE SET NULL,
    ai_call_id uuid,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT contact_candidates_raw_not_blank CHECK (length(trim(raw_value)) > 0),
    CONSTRAINT contact_candidates_normalized_not_blank CHECK (length(trim(normalized_value)) > 0),
    CONSTRAINT contact_candidates_unique UNIQUE (
        run_id, interviewee_id, origin_id, contact_type, normalized_value
    )
);

CREATE TABLE ai_calls (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id uuid REFERENCES pipeline_runs(id) ON DELETE SET NULL,
    job_id uuid REFERENCES jobs(id) ON DELETE SET NULL,
    purpose text NOT NULL CHECK (purpose IN (
        'guest_extraction', 'contact_extraction', 'categorization',
        'identity_resolution', 'page_relevance', 'summary', 'other'
    )),
    model text NOT NULL,
    prompt_version text NOT NULL,
    schema_version text NOT NULL,
    input_hash text NOT NULL,
    input_payload jsonb,
    output_payload jsonb,
    status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'succeeded', 'failed', 'invalid_output')),
    prompt_tokens integer CHECK (prompt_tokens IS NULL OR prompt_tokens >= 0),
    completion_tokens integer CHECK (completion_tokens IS NULL OR completion_tokens >= 0),
    cost_micros bigint CHECK (cost_micros IS NULL OR cost_micros >= 0),
    latency_ms bigint CHECK (latency_ms IS NULL OR latency_ms >= 0),
    error_code text,
    error_message text,
    retain_until timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    finished_at timestamptz,
    CONSTRAINT ai_calls_model_not_blank CHECK (length(trim(model)) > 0),
    CONSTRAINT ai_calls_input_hash_not_blank CHECK (length(trim(input_hash)) > 0)
);

ALTER TABLE interviewee_candidates
    ADD CONSTRAINT interviewee_candidates_ai_call_fk
    FOREIGN KEY (ai_call_id) REFERENCES ai_calls(id) ON DELETE SET NULL;

ALTER TABLE contact_candidates
    ADD CONSTRAINT contact_candidates_ai_call_fk
    FOREIGN KEY (ai_call_id) REFERENCES ai_calls(id) ON DELETE SET NULL;

CREATE TABLE audit_events (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id uuid REFERENCES pipeline_runs(id) ON DELETE SET NULL,
    actor_type text NOT NULL DEFAULT 'system' CHECK (actor_type IN ('system', 'ai', 'user', 'worker')),
    actor_id uuid,
    action text NOT NULL,
    entity_type text NOT NULL,
    entity_id uuid,
    before_data jsonb,
    after_data jsonb,
    metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT audit_events_action_not_blank CHECK (length(trim(action)) > 0),
    CONSTRAINT audit_events_entity_type_not_blank CHECK (length(trim(entity_type)) > 0)
);

CREATE TABLE suppression_entries (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    scope_kind text NOT NULL CHECK (scope_kind IN ('contact', 'interviewee', 'domain')),
    contact_type text,
    normalized_hash text NOT NULL,
    reason text NOT NULL DEFAULT '',
    created_by uuid,
    expires_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT suppression_entries_hash_not_blank CHECK (length(trim(normalized_hash)) > 0),
    CONSTRAINT suppression_entries_unique UNIQUE NULLS NOT DISTINCT (scope_kind, contact_type, normalized_hash)
);

CREATE INDEX podcast_channels_name_trgm_idx ON podcast_channels USING gin (name gin_trgm_ops);
CREATE INDEX podcast_channels_status_idx ON podcast_channels (status) WHERE deleted_at IS NULL;
CREATE INDEX videos_channel_published_idx ON videos (channel_id, published_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX transcripts_video_idx ON transcripts (video_id);
CREATE INDEX categories_name_trgm_idx ON categories USING gin (normalized_name gin_trgm_ops) WHERE active;
CREATE INDEX interviewees_display_name_trgm_idx ON interviewees USING gin (normalized_display_name gin_trgm_ops)
    WHERE deleted_at IS NULL AND status = 'active';
CREATE INDEX interviewees_category_idx ON interviewees (primary_category_id) WHERE deleted_at IS NULL;
CREATE INDEX interviewee_aliases_name_trgm_idx ON interviewee_aliases USING gin (normalized_alias gin_trgm_ops);
CREATE INDEX appearances_video_idx ON appearances (video_id);
CREATE INDEX origins_domain_idx ON origins (domain);
CREATE INDEX contacts_interviewee_idx ON contacts (interviewee_id) WHERE deleted_at IS NULL;
CREATE INDEX contacts_type_value_idx ON contacts (contact_type, normalized_value) WHERE deleted_at IS NULL;
CREATE INDEX contact_evidence_contact_idx ON contact_evidence (contact_id, collected_at DESC);
CREATE INDEX pipeline_runs_status_created_idx ON pipeline_runs (status, created_at DESC);
CREATE INDEX jobs_claim_idx ON jobs (priority DESC, available_at, created_at)
    WHERE status IN ('queued', 'retry_scheduled') AND cancel_requested_at IS NULL;
CREATE INDEX jobs_run_status_idx ON jobs (run_id, status);
CREATE INDEX jobs_locked_idx ON jobs (locked_at) WHERE status = 'running';
CREATE INDEX job_attempts_job_idx ON job_attempts (job_id, attempt_no DESC);
CREATE INDEX crawl_pages_frontier_idx ON crawl_pages (run_id, depth, queued_at) WHERE status = 'queued';
CREATE INDEX crawl_pages_interviewee_idx ON crawl_pages (interviewee_id, status);
CREATE INDEX crawl_edges_from_idx ON crawl_edges (from_page_id);
CREATE INDEX interviewee_candidates_status_idx ON interviewee_candidates (run_id, status);
CREATE INDEX contact_candidates_status_idx ON contact_candidates (run_id, status);
CREATE INDEX ai_calls_run_created_idx ON ai_calls (run_id, created_at DESC);
CREATE INDEX ai_calls_purpose_status_idx ON ai_calls (purpose, status);
CREATE INDEX audit_events_entity_idx ON audit_events (entity_type, entity_id, created_at DESC);
CREATE INDEX suppression_entries_lookup_idx ON suppression_entries (normalized_hash, scope_kind);

CREATE TRIGGER media_assets_touch_updated_at BEFORE UPDATE ON media_assets
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER podcast_channels_touch_updated_at BEFORE UPDATE ON podcast_channels
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER videos_touch_updated_at BEFORE UPDATE ON videos
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER transcripts_touch_updated_at BEFORE UPDATE ON transcripts
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER categories_touch_updated_at BEFORE UPDATE ON categories
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER pipeline_runs_touch_updated_at BEFORE UPDATE ON pipeline_runs
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER interviewees_touch_updated_at BEFORE UPDATE ON interviewees
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER appearances_touch_updated_at BEFORE UPDATE ON appearances
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER origins_touch_updated_at BEFORE UPDATE ON origins
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER contacts_touch_updated_at BEFORE UPDATE ON contacts
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER jobs_touch_updated_at BEFORE UPDATE ON jobs
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER crawl_pages_touch_updated_at BEFORE UPDATE ON crawl_pages
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER interviewee_candidates_touch_updated_at BEFORE UPDATE ON interviewee_candidates
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
CREATE TRIGGER contact_candidates_touch_updated_at BEFORE UPDATE ON contact_candidates
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();

INSERT INTO schema_migrations(version)
VALUES ('0001_initial')
ON CONFLICT (version) DO NOTHING;

COMMIT;
