-- AlphaX Agent ｜ Crypto PostgreSQL initial schema.
-- Keep column names compatible with the current SQLite data model.

CREATE TABLE IF NOT EXISTS schema_migrations (
    version TEXT PRIMARY KEY,
    applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS coin_state (
    symbol TEXT PRIMARY KEY,
    state TEXT NOT NULL DEFAULT '蓄力',
    score INTEGER DEFAULT 0,
    anomaly_type TEXT DEFAULT '',
    sector TEXT DEFAULT '',
    leader_status TEXT DEFAULT '',
    detected_at TEXT NOT NULL,
    last_alert_time TEXT DEFAULT '',
    last_alert_level TEXT DEFAULT '',
    detail_json TEXT DEFAULT '{}'
);

CREATE TABLE IF NOT EXISTS screening_log (
    id BIGSERIAL PRIMARY KEY,
    scan_time TEXT NOT NULL,
    layer TEXT NOT NULL,
    symbol TEXT NOT NULL,
    state TEXT NOT NULL,
    score INTEGER DEFAULT 0,
    price DOUBLE PRECISION NOT NULL,
    signals TEXT DEFAULT '',
    sector TEXT DEFAULT '',
    leader_status TEXT DEFAULT '',
    is_meme INTEGER DEFAULT 0,
    change_24h DOUBLE PRECISION DEFAULT 0,
    funding_rate DOUBLE PRECISION DEFAULT 0,
    detail_json TEXT DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS idx_screening_log_time ON screening_log(scan_time DESC);
CREATE INDEX IF NOT EXISTS idx_screening_log_layer_time ON screening_log(layer, scan_time DESC);
CREATE INDEX IF NOT EXISTS idx_screening_log_symbol_time ON screening_log(symbol, scan_time DESC);

CREATE TABLE IF NOT EXISTS recommendation (
    id BIGSERIAL PRIMARY KEY,
    symbol TEXT NOT NULL,
    rec_time TEXT NOT NULL,
    rec_state TEXT NOT NULL,
    rec_score INTEGER DEFAULT 0,
    entry_price DOUBLE PRECISION NOT NULL,
    stop_loss DOUBLE PRECISION DEFAULT 0,
    tp1 DOUBLE PRECISION DEFAULT 0,
    tp2 DOUBLE PRECISION DEFAULT 0,
    sector TEXT DEFAULT '',
    signals TEXT DEFAULT '',
    is_meme INTEGER DEFAULT 0,
    status TEXT DEFAULT 'active',
    current_price DOUBLE PRECISION DEFAULT 0,
    max_price DOUBLE PRECISION DEFAULT 0,
    min_price DOUBLE PRECISION DEFAULT 0,
    pnl_pct DOUBLE PRECISION DEFAULT 0,
    max_pnl_pct DOUBLE PRECISION DEFAULT 0,
    max_drawdown_pct DOUBLE PRECISION DEFAULT 0,
    hit_tp1_time TEXT DEFAULT '',
    hit_tp2_time TEXT DEFAULT '',
    stopped_out_time TEXT DEFAULT '',
    expired_time TEXT DEFAULT '',
    last_track_time TEXT DEFAULT '',
    entry_plan_json TEXT DEFAULT '{}',
    action_status TEXT DEFAULT '持有',
    strategy_version TEXT DEFAULT '',
    direction TEXT DEFAULT '中性',
    force_reason TEXT DEFAULT '',
    base_state TEXT DEFAULT '',
    sector_signal_count INTEGER DEFAULT 0,
    market_context_json TEXT DEFAULT '{}',
    derivatives_context_json TEXT DEFAULT '{}',
    sector_context_json TEXT DEFAULT '{}',
    lifecycle_state TEXT DEFAULT 'watching',
    display_bucket TEXT DEFAULT 'watch_pool',
    execution_status TEXT DEFAULT 'observe',
    state_reason TEXT DEFAULT '',
    entry_triggered INTEGER DEFAULT 0,
    archived_at TEXT DEFAULT '',
    signal_codes_json TEXT DEFAULT '[]',
    signal_labels_json TEXT DEFAULT '[]'
);
CREATE INDEX IF NOT EXISTS idx_rec_active_symbol_bucket ON recommendation(symbol, status, display_bucket, id DESC);
CREATE INDEX IF NOT EXISTS idx_rec_display_bucket_time ON recommendation(display_bucket, rec_time DESC);
CREATE INDEX IF NOT EXISTS idx_rec_symbol_time ON recommendation(symbol, rec_time DESC);

CREATE TABLE IF NOT EXISTS price_tracking (
    id BIGSERIAL PRIMARY KEY,
    rec_id BIGINT NOT NULL,
    symbol TEXT NOT NULL,
    track_time TEXT NOT NULL,
    price DOUBLE PRECISION NOT NULL,
    pnl_pct DOUBLE PRECISION DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_price_tracking_rec_id_id ON price_tracking(rec_id, id DESC);
CREATE INDEX IF NOT EXISTS idx_price_tracking_rec_time ON price_tracking(rec_id, track_time DESC);

CREATE TABLE IF NOT EXISTS latest_price_cache (
    symbol TEXT PRIMARY KEY,
    price DOUBLE PRECISION NOT NULL,
    updated_at TEXT NOT NULL,
    source TEXT DEFAULT 'tracker'
);
CREATE INDEX IF NOT EXISTS idx_latest_price_cache_updated_at ON latest_price_cache(updated_at);

CREATE TABLE IF NOT EXISTS cron_run_log (
    id BIGSERIAL PRIMARY KEY,
    job_name TEXT NOT NULL,
    script_name TEXT NOT NULL,
    run_status TEXT NOT NULL,
    result_status TEXT DEFAULT '',
    started_at TEXT NOT NULL,
    finished_at TEXT NOT NULL,
    duration_ms INTEGER DEFAULT 0,
    summary_json TEXT DEFAULT '{}',
    error_message TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_cron_run_log_job_started ON cron_run_log(job_name, started_at DESC);

CREATE TABLE IF NOT EXISTS signal_performance (
    signal_type TEXT PRIMARY KEY,
    category TEXT DEFAULT '',
    total_count INTEGER DEFAULT 0,
    hit_count INTEGER DEFAULT 0,
    miss_count INTEGER DEFAULT 0,
    hit_rate DOUBLE PRECISION DEFAULT 0,
    avg_pnl DOUBLE PRECISION DEFAULT 0,
    weight DOUBLE PRECISION DEFAULT 1.0,
    last_updated TEXT DEFAULT ''
);

CREATE TABLE IF NOT EXISTS review_log (
    id BIGSERIAL PRIMARY KEY,
    rec_id BIGINT NOT NULL,
    symbol TEXT NOT NULL,
    review_time TEXT NOT NULL,
    outcome TEXT NOT NULL,
    pnl_48h DOUBLE PRECISION DEFAULT 0,
    max_pnl_48h DOUBLE PRECISION DEFAULT 0,
    triggered_signals TEXT DEFAULT '',
    hit_signals TEXT DEFAULT '',
    miss_signals TEXT DEFAULT '',
    lesson TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_review_log_rec_id ON review_log(rec_id);
CREATE INDEX IF NOT EXISTS idx_review_log_time ON review_log(review_time DESC);

CREATE TABLE IF NOT EXISTS missed_explosions (
    id BIGSERIAL PRIMARY KEY,
    symbol TEXT NOT NULL,
    detect_time TEXT NOT NULL,
    price_at_detect DOUBLE PRECISION DEFAULT 0,
    price_before DOUBLE PRECISION DEFAULT 0,
    gain_pct DOUBLE PRECISION DEFAULT 0,
    reason_missed TEXT DEFAULT '',
    features_detected TEXT DEFAULT '',
    lesson TEXT DEFAULT ''
);

CREATE TABLE IF NOT EXISTS strategy_iteration_log (
    id BIGSERIAL PRIMARY KEY,
    run_date TEXT NOT NULL,
    created_at TEXT NOT NULL,
    trigger_source TEXT DEFAULT 'daily_review',
    title TEXT NOT NULL,
    summary TEXT DEFAULT '',
    findings_json TEXT DEFAULT '[]',
    problems_json TEXT DEFAULT '[]',
    actions_json TEXT DEFAULT '[]',
    changed_rules_json TEXT DEFAULT '[]',
    metrics_json TEXT DEFAULT '{}',
    related_symbols_json TEXT DEFAULT '[]',
    config_diff_json TEXT DEFAULT '{}',
    effect_summary_json TEXT DEFAULT '{}',
    pollution_summary_json TEXT DEFAULT '{}',
    strategy_version TEXT DEFAULT '',
    version_change_summary TEXT DEFAULT '',
    success_analysis_json TEXT DEFAULT '{}',
    failure_analysis_json TEXT DEFAULT '{}',
    candidate_rules_json TEXT DEFAULT '[]',
    release_decision TEXT DEFAULT '',
    release_reason TEXT DEFAULT '',
    confidence_level TEXT DEFAULT '',
    promotion_state TEXT DEFAULT 'research_only'
);

CREATE TABLE IF NOT EXISTS strategy_rule_candidate (
    id BIGSERIAL PRIMARY KEY,
    created_at TEXT NOT NULL,
    source TEXT DEFAULT '',
    rule_type TEXT DEFAULT '',
    signal_name TEXT DEFAULT '',
    rule_description TEXT DEFAULT '',
    support_count INTEGER DEFAULT 0,
    success_count INTEGER DEFAULT 0,
    fail_count INTEGER DEFAULT 0,
    avg_pnl DOUBLE PRECISION DEFAULT 0,
    max_gain DOUBLE PRECISION DEFAULT 0,
    max_drawdown DOUBLE PRECISION DEFAULT 0,
    confidence_score DOUBLE PRECISION DEFAULT 0,
    sample_size INTEGER DEFAULT 0,
    status TEXT DEFAULT 'candidate',
    release_version TEXT DEFAULT '',
    notes TEXT DEFAULT '',
    source_ref TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_rule_candidate_status ON strategy_rule_candidate(status, created_at);

CREATE TABLE IF NOT EXISTS strategy_failure_pattern (
    id BIGSERIAL PRIMARY KEY,
    created_at TEXT NOT NULL,
    symbol TEXT NOT NULL,
    version TEXT DEFAULT '',
    failure_type TEXT DEFAULT '',
    failure_reason TEXT DEFAULT '',
    signal_combo TEXT DEFAULT '[]',
    market_context_json TEXT DEFAULT '{}',
    entry_quality_issue TEXT DEFAULT '',
    pnl_pct DOUBLE PRECISION DEFAULT 0,
    max_drawdown_pct DOUBLE PRECISION DEFAULT 0,
    lesson TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_failure_pattern_type ON strategy_failure_pattern(failure_type, created_at);

CREATE TABLE IF NOT EXISTS push_log (
    id BIGSERIAL PRIMARY KEY,
    symbol TEXT NOT NULL,
    push_type TEXT NOT NULL,
    action_status TEXT DEFAULT '',
    pushed_at TEXT NOT NULL,
    rec_id BIGINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_push_lookup ON push_log(symbol, push_type, pushed_at);
CREATE INDEX IF NOT EXISTS idx_push_log_rec_action ON push_log(rec_id, push_type, action_status, pushed_at);

CREATE TABLE IF NOT EXISTS sentiment_events (
    id BIGSERIAL PRIMARY KEY,
    symbol TEXT NOT NULL,
    name TEXT DEFAULT '',
    source TEXT NOT NULL,
    event_type TEXT NOT NULL,
    trend_rank INTEGER DEFAULT 0,
    trend_score INTEGER DEFAULT 0,
    market_cap_rank INTEGER DEFAULT 0,
    extra_json TEXT DEFAULT '{}',
    detected_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sentiment_lookup ON sentiment_events(symbol, source, detected_at);

CREATE TABLE IF NOT EXISTS llm_insights (
    id BIGSERIAL PRIMARY KEY,
    target_type TEXT NOT NULL,
    target_id TEXT NOT NULL,
    insight_type TEXT NOT NULL,
    prompt_version TEXT NOT NULL,
    input_hash TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'success',
    input_json TEXT DEFAULT '{}',
    content_json TEXT DEFAULT '{}',
    error TEXT DEFAULT '',
    model TEXT DEFAULT '',
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_llm_insights_unique
ON llm_insights(target_type, target_id, insight_type, input_hash);
CREATE INDEX IF NOT EXISTS idx_llm_insights_lookup
ON llm_insights(target_type, target_id, insight_type, status, updated_at);

CREATE TABLE IF NOT EXISTS event_news (
    id BIGSERIAL PRIMARY KEY,
    event_hash TEXT UNIQUE,
    source TEXT NOT NULL,
    symbol TEXT NOT NULL,
    title TEXT NOT NULL,
    url TEXT DEFAULT '',
    published_at TEXT NOT NULL,
    detected_at TEXT NOT NULL,
    importance TEXT DEFAULT 'B',
    event_type TEXT DEFAULT '',
    raw_json TEXT DEFAULT '{}',
    processed INTEGER DEFAULT 0,
    decision TEXT DEFAULT '',
    tech_score INTEGER DEFAULT 0,
    rec_id BIGINT DEFAULT 0,
    pushed INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_event_news_time ON event_news(published_at, detected_at);
CREATE INDEX IF NOT EXISTS idx_event_news_symbol ON event_news(symbol);

CREATE TABLE IF NOT EXISTS onchain_token_map (
    id BIGSERIAL PRIMARY KEY,
    symbol TEXT NOT NULL,
    chain TEXT NOT NULL,
    contract_address TEXT NOT NULL,
    source TEXT DEFAULT '',
    confidence INTEGER DEFAULT 0,
    is_active INTEGER DEFAULT 1,
    raw_json TEXT DEFAULT '{}',
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    UNIQUE(symbol, chain, contract_address)
);
CREATE INDEX IF NOT EXISTS idx_onchain_token_map_symbol ON onchain_token_map(symbol, confidence, is_active);

CREATE TABLE IF NOT EXISTS onchain_events (
    id BIGSERIAL PRIMARY KEY,
    event_hash TEXT UNIQUE,
    chain TEXT NOT NULL,
    symbol TEXT NOT NULL,
    contract_address TEXT DEFAULT '',
    event_type TEXT NOT NULL,
    signal_code TEXT NOT NULL,
    signal_label TEXT DEFAULT '',
    direction TEXT DEFAULT 'neutral',
    value_usd DOUBLE PRECISION DEFAULT 0,
    amount DOUBLE PRECISION DEFAULT 0,
    tx_hash TEXT DEFAULT '',
    wallet_address TEXT DEFAULT '',
    wallet_label TEXT DEFAULT '',
    counterparty_label TEXT DEFAULT '',
    confidence INTEGER DEFAULT 0,
    severity TEXT DEFAULT 'B',
    status TEXT DEFAULT 'new',
    detected_at TEXT NOT NULL,
    source TEXT DEFAULT '',
    url TEXT DEFAULT '',
    raw_json TEXT DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS idx_onchain_events_time ON onchain_events(detected_at, signal_code);
CREATE INDEX IF NOT EXISTS idx_onchain_events_symbol ON onchain_events(symbol, detected_at);
CREATE INDEX IF NOT EXISTS idx_onchain_events_chain ON onchain_events(chain, detected_at);

CREATE TABLE IF NOT EXISTS onchain_token_metrics (
    id BIGSERIAL PRIMARY KEY,
    symbol TEXT NOT NULL,
    chain TEXT NOT NULL,
    contract_address TEXT DEFAULT '',
    "window" TEXT NOT NULL,
    metric_time TEXT NOT NULL,
    dex_volume_usd DOUBLE PRECISION DEFAULT 0,
    dex_volume_change_pct DOUBLE PRECISION DEFAULT 0,
    liquidity_usd DOUBLE PRECISION DEFAULT 0,
    liquidity_change_pct DOUBLE PRECISION DEFAULT 0,
    exchange_netflow_usd DOUBLE PRECISION DEFAULT 0,
    whale_accumulation_usd DOUBLE PRECISION DEFAULT 0,
    holder_delta DOUBLE PRECISION DEFAULT 0,
    smart_money_score DOUBLE PRECISION DEFAULT 0,
    onchain_score DOUBLE PRECISION DEFAULT 0,
    risk_score DOUBLE PRECISION DEFAULT 0,
    source TEXT DEFAULT '',
    raw_json TEXT DEFAULT '{}',
    UNIQUE(symbol, chain, contract_address, "window", metric_time)
);
CREATE INDEX IF NOT EXISTS idx_onchain_metrics_symbol ON onchain_token_metrics(symbol, metric_time);

CREATE TABLE IF NOT EXISTS onchain_raw_events (
    id BIGSERIAL PRIMARY KEY,
    event_hash TEXT UNIQUE,
    source TEXT NOT NULL,
    chain TEXT NOT NULL,
    event_type TEXT NOT NULL,
    token_address TEXT DEFAULT '',
    symbol_guess TEXT DEFAULT '',
    name TEXT DEFAULT '',
    title TEXT DEFAULT '',
    description TEXT DEFAULT '',
    url TEXT DEFAULT '',
    icon TEXT DEFAULT '',
    amount DOUBLE PRECISION DEFAULT 0,
    total_amount DOUBLE PRECISION DEFAULT 0,
    importance DOUBLE PRECISION DEFAULT 0,
    mapped_symbol TEXT DEFAULT '',
    mapping_status TEXT DEFAULT 'unmapped',
    detected_at TEXT NOT NULL,
    raw_json TEXT DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS idx_onchain_raw_events_time ON onchain_raw_events(detected_at, importance);
CREATE INDEX IF NOT EXISTS idx_onchain_raw_events_chain ON onchain_raw_events(chain, detected_at);
CREATE INDEX IF NOT EXISTS idx_onchain_raw_events_mapping ON onchain_raw_events(mapping_status, detected_at);

CREATE TABLE IF NOT EXISTS app_user (
    id BIGSERIAL PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    password_salt TEXT NOT NULL,
    email_verified INTEGER DEFAULT 0,
    status TEXT DEFAULT 'pending_email_verification',
    invite_code TEXT NOT NULL UNIQUE,
    invited_by_user_id BIGINT,
    free_trial_claimed INTEGER DEFAULT 0,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    last_login_at TEXT DEFAULT '',
    is_admin INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_app_user_email ON app_user(email);
CREATE INDEX IF NOT EXISTS idx_app_user_invite_code ON app_user(invite_code);

CREATE TABLE IF NOT EXISTS email_verification_code (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    email TEXT NOT NULL,
    code_hash TEXT NOT NULL,
    purpose TEXT NOT NULL DEFAULT 'register',
    expires_at TEXT NOT NULL,
    used_at TEXT DEFAULT '',
    created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_email_code_lookup ON email_verification_code(email, purpose, used_at);

CREATE TABLE IF NOT EXISTS user_session (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    token_hash TEXT NOT NULL UNIQUE,
    created_at TEXT NOT NULL,
    expires_at TEXT NOT NULL,
    revoked_at TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_user_session_token ON user_session(token_hash);

CREATE TABLE IF NOT EXISTS subscription_plan (
    code TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    duration_days INTEGER NOT NULL,
    price_usdt DOUBLE PRECISION DEFAULT 0,
    status TEXT DEFAULT 'active',
    sort_order INTEGER DEFAULT 0,
    created_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS user_subscription (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    plan_code TEXT NOT NULL,
    start_at TEXT NOT NULL,
    end_at TEXT NOT NULL,
    status TEXT DEFAULT 'active',
    source TEXT NOT NULL,
    order_id BIGINT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_user_subscription_user ON user_subscription(user_id, status, end_at);

CREATE TABLE IF NOT EXISTS payment_order (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    plan_code TEXT NOT NULL,
    amount_usdt DOUBLE PRECISION NOT NULL DEFAULT 0,
    chain TEXT NOT NULL DEFAULT 'TRC20',
    pay_address TEXT DEFAULT '',
    txid TEXT DEFAULT '',
    status TEXT NOT NULL DEFAULT 'pending',
    created_at TEXT NOT NULL,
    paid_at TEXT DEFAULT '',
    expire_at TEXT DEFAULT '',
    admin_note TEXT DEFAULT '',
    raw_payload_json TEXT DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS idx_payment_order_user ON payment_order(user_id, status);
CREATE INDEX IF NOT EXISTS idx_payment_order_txid ON payment_order(txid);
CREATE INDEX IF NOT EXISTS idx_payment_order_created ON payment_order(created_at);

CREATE TABLE IF NOT EXISTS pending_registration (
    id BIGSERIAL PRIMARY KEY,
    email TEXT NOT NULL,
    code_hash TEXT NOT NULL,
    expires_at TEXT NOT NULL,
    used_at TEXT DEFAULT '',
    created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pending_reg_email ON pending_registration(email, used_at);

CREATE TABLE IF NOT EXISTS referral_reward (
    id BIGSERIAL PRIMARY KEY,
    inviter_user_id BIGINT NOT NULL,
    invitee_user_id BIGINT NOT NULL,
    order_id BIGINT,
    reward_type TEXT NOT NULL DEFAULT 'days',
    reward_days INTEGER DEFAULT 0,
    reward_amount_usdt DOUBLE PRECISION DEFAULT 0,
    status TEXT DEFAULT 'pending',
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS user_activity (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    action TEXT NOT NULL,
    page TEXT DEFAULT '',
    ip TEXT DEFAULT '',
    created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_ua_user ON user_activity(user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_ua_date ON user_activity(created_at);

CREATE TABLE IF NOT EXISTS user_watchlist (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    symbol TEXT NOT NULL,
    created_at TEXT NOT NULL,
    UNIQUE(user_id, symbol)
);
CREATE INDEX IF NOT EXISTS idx_user_watchlist_user ON user_watchlist(user_id, symbol);

CREATE TABLE IF NOT EXISTS user_saved_observation (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    rec_id BIGINT NOT NULL,
    note TEXT DEFAULT '',
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    UNIQUE(user_id, rec_id)
);
CREATE INDEX IF NOT EXISTS idx_saved_obs_user ON user_saved_observation(user_id, rec_id);

CREATE TABLE IF NOT EXISTS user_push_rule (
    user_id BIGINT PRIMARY KEY,
    watchlist_only INTEGER DEFAULT 0,
    min_score INTEGER DEFAULT 0,
    min_rr DOUBLE PRECISION DEFAULT 0,
    push_buy_now INTEGER DEFAULT 1,
    push_wait_pullback INTEGER DEFAULT 1,
    push_observe INTEGER DEFAULT 0,
    quiet_start TEXT DEFAULT '',
    quiet_end TEXT DEFAULT '',
    updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS system_reset_log (
    id BIGSERIAL PRIMARY KEY,
    reset_at TEXT NOT NULL,
    reason TEXT DEFAULT '',
    backup_path TEXT DEFAULT '',
    counts_json TEXT DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS idx_system_reset_log_time ON system_reset_log(reset_at DESC);

CREATE TABLE IF NOT EXISTS scheduler_job_config (
    job_name TEXT PRIMARY KEY,
    command TEXT NOT NULL,
    args_json TEXT DEFAULT '[]',
    enabled INTEGER DEFAULT 1,
    every_seconds INTEGER NOT NULL,
    initial_delay INTEGER DEFAULT 0,
    lock_group TEXT DEFAULT '',
    description TEXT DEFAULT '',
    sort_order INTEGER DEFAULT 0,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS scheduler_runtime_status (
    job_name TEXT PRIMARY KEY,
    status TEXT DEFAULT 'idle',
    pid INTEGER DEFAULT 0,
    run_kind TEXT DEFAULT '',
    trigger_id BIGINT DEFAULT 0,
    locked_by TEXT DEFAULT '',
    next_run_at TEXT DEFAULT '',
    last_started_at TEXT DEFAULT '',
    last_finished_at TEXT DEFAULT '',
    last_exit_code INTEGER DEFAULT 0,
    last_duration_ms INTEGER DEFAULT 0,
    last_error TEXT DEFAULT '',
    output_tail TEXT DEFAULT '',
    updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS scheduler_manual_trigger (
    id BIGSERIAL PRIMARY KEY,
    job_name TEXT NOT NULL,
    force INTEGER DEFAULT 0,
    status TEXT DEFAULT 'queued',
    requested_by TEXT DEFAULT '',
    requested_at TEXT NOT NULL,
    started_at TEXT DEFAULT '',
    finished_at TEXT DEFAULT '',
    exit_code INTEGER DEFAULT 0,
    duration_ms INTEGER DEFAULT 0,
    output_tail TEXT DEFAULT '',
    error_message TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_scheduler_trigger_status ON scheduler_manual_trigger(status, requested_at);
