CREATE TABLE IF NOT EXISTS ops.scheduler_jobs (
    job_key             VARCHAR(96) PRIMARY KEY,
    display_name        VARCHAR(120) NOT NULL,
    category            VARCHAR(64) NOT NULL,
    description         TEXT,
    enabled             BOOLEAN NOT NULL DEFAULT true,
    schedule_type       VARCHAR(20) NOT NULL DEFAULT 'interval',
    interval_seconds    INT NOT NULL DEFAULT 300,
    cron_expr           VARCHAR(120),
    timezone            VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
    timeout_seconds     INT NOT NULL DEFAULT 30,
    batch_size          INT,
    max_concurrency     INT NOT NULL DEFAULT 1,
    config              JSONB NOT NULL DEFAULT '{}'::jsonb,
    version             INT NOT NULL DEFAULT 1,
    updated_by          BIGINT,
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CHECK (job_key ~ '^[a-z0-9_:-]+$'),
    CHECK (schedule_type IN ('interval', 'cron', 'manual')),
    CHECK (interval_seconds > 0),
    CHECK (timeout_seconds > 0),
    CHECK (max_concurrency > 0)
);

CREATE INDEX IF NOT EXISTS idx_scheduler_jobs_category
    ON ops.scheduler_jobs (category, job_key);

CREATE TABLE IF NOT EXISTS ops.scheduler_instances (
    instance_id   VARCHAR(160) PRIMARY KEY,
    hostname      VARCHAR(255) NOT NULL,
    process       VARCHAR(80) NOT NULL,
    build_version VARCHAR(120),
    started_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_seen_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    metadata      JSONB NOT NULL DEFAULT '{}'::jsonb
);

CREATE INDEX IF NOT EXISTS idx_scheduler_instances_seen
    ON ops.scheduler_instances (last_seen_at DESC);

CREATE TABLE IF NOT EXISTS ops.scheduler_job_runs (
    id                    BIGSERIAL PRIMARY KEY,
    job_key               VARCHAR(96) NOT NULL REFERENCES ops.scheduler_jobs(job_key),
    trigger_id            BIGINT,
    scheduler_instance_id VARCHAR(160),
    trigger_type          VARCHAR(30) NOT NULL DEFAULT 'auto',
    status                VARCHAR(30) NOT NULL DEFAULT 'running',
    config_version        INT NOT NULL DEFAULT 1,
    config_snapshot       JSONB NOT NULL DEFAULT '{}'::jsonb,
    stats                 JSONB NOT NULL DEFAULT '{}'::jsonb,
    error_message         TEXT,
    started_at            TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    finished_at           TIMESTAMPTZ,
    duration_ms           BIGINT,
    CHECK (trigger_type IN ('auto', 'manual', 'dry_run')),
    CHECK (status IN ('running', 'success', 'failed', 'skipped'))
);

CREATE INDEX IF NOT EXISTS idx_scheduler_job_runs_job_started
    ON ops.scheduler_job_runs (job_key, started_at DESC, id DESC);

CREATE INDEX IF NOT EXISTS idx_scheduler_job_runs_status_started
    ON ops.scheduler_job_runs (status, started_at DESC);

CREATE INDEX IF NOT EXISTS idx_scheduler_job_runs_auto_started
    ON ops.scheduler_job_runs (job_key, started_at DESC, id DESC)
    WHERE trigger_type = 'auto';

CREATE TABLE IF NOT EXISTS ops.scheduler_job_triggers (
    id                    BIGSERIAL PRIMARY KEY,
    job_key               VARCHAR(96) NOT NULL REFERENCES ops.scheduler_jobs(job_key),
    trigger_type          VARCHAR(30) NOT NULL DEFAULT 'manual',
    status                VARCHAR(30) NOT NULL DEFAULT 'pending',
    requested_by          BIGINT,
    reason                TEXT,
    config_override       JSONB NOT NULL DEFAULT '{}'::jsonb,
    scheduler_instance_id VARCHAR(160),
    run_id                BIGINT REFERENCES ops.scheduler_job_runs(id),
    requested_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    claimed_at            TIMESTAMPTZ,
    finished_at           TIMESTAMPTZ,
    error_message         TEXT,
    CHECK (trigger_type IN ('manual', 'dry_run')),
    CHECK (status IN ('pending', 'claimed', 'success', 'failed', 'skipped'))
);

CREATE INDEX IF NOT EXISTS idx_scheduler_job_triggers_pending
    ON ops.scheduler_job_triggers (job_key, requested_at ASC, id ASC)
    WHERE status = 'pending';

INSERT INTO ops.scheduler_jobs
    (job_key, display_name, category, description, enabled, schedule_type, interval_seconds, timezone, timeout_seconds, batch_size, config)
VALUES
    ('schedule_dispatch', '内容定时任务派发', 'content', '索引驱动的延迟队列派发，仅认领到期内容任务并投递生成队列。', true, 'interval', 15, 'Asia/Shanghai', 30, 100, '{}'::jsonb),
    ('monitoring_daily_task', 'AI 监控每日任务生成', 'monitoring', '客户端在线时按业务日滚动创建并派发监控采集任务，离线不生成待采积压。', true, 'interval', 300, 'Asia/Shanghai', 45, 12, '{"max_desktop_client_backlog":12}'::jsonb),
    ('monitoring_result_recovery', '监控结果恢复', 'monitoring', '按回调结果队列索引恢复未成功入队的监控结果。', true, 'interval', 60, 'Asia/Shanghai', 30, 100, '{}'::jsonb),
    ('monitoring_lease_recovery', '监控租约回收', 'monitoring', '按过期租约索引限批回收超过租约时间仍未回调的监控任务。', true, 'interval', 1800, 'Asia/Shanghai', 30, 1000, '{}'::jsonb),
    ('monitoring_received_inspection', '监控 received 卡死检查', 'monitoring', '按 received watchdog 索引限批标记长时间未入队的监控任务。', true, 'interval', 300, 'Asia/Shanghai', 30, 100, '{"threshold":"15m"}'::jsonb),
    ('generation_state_check', '生成任务状态巡检', 'generation', '低频、限批检查文章生成任务与文章状态一致性。', true, 'interval', 300, 'Asia/Shanghai', 10, 100, '{"lookback":"24h"}'::jsonb),
    ('monitoring_retention_cleanup', '监控历史保留清理', 'monitoring', '按保留窗口和索引限批删除最近 30 天以前的监控历史数据。', true, 'interval', 86400, 'Asia/Shanghai', 900, 5000,
     '{"retention_days":30,"batch_size":5000,"max_batches_per_run":200,"dry_run":false,"statement_timeout":"5s","run_at_local":"02:30"}'::jsonb)
ON CONFLICT (job_key) DO UPDATE
SET display_name = EXCLUDED.display_name,
    category = EXCLUDED.category,
    description = EXCLUDED.description,
    config = ops.scheduler_jobs.config || EXCLUDED.config,
    updated_at = NOW();
