-- MikrotikManager PostgreSQL 18 schema (not 1:1 SQLite).
-- Applied once at backend boot via src/db/migrate.ts

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

CREATE TABLE IF NOT EXISTS servers (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name TEXT NOT NULL DEFAULT '',
  host TEXT NOT NULL,
  port INTEGER NOT NULL DEFAULT 443,
  username TEXT NOT NULL DEFAULT 'admin',
  password TEXT NOT NULL DEFAULT '',
  use_ssl BOOLEAN NOT NULL DEFAULT TRUE,
  verify_ssl BOOLEAN NOT NULL DEFAULT FALSE,
  type TEXT NOT NULL DEFAULT 'home-router'
    CHECK (type IN ('jump-host', 'exit-node', 'home-router')),
  site TEXT NOT NULL DEFAULT '',
  country TEXT NOT NULL DEFAULT '',
  asn TEXT NOT NULL DEFAULT '',
  comment TEXT NOT NULL DEFAULT '',
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  lan_subnet TEXT NOT NULL DEFAULT '',
  wan_uplinks JSONB NOT NULL DEFAULT '[]'::jsonb,
  mgmt_tunnel_ip TEXT NOT NULL DEFAULT '',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS traffic_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  interval_sec INTEGER NOT NULL DEFAULT 30,
  retention_days INTEGER NOT NULL DEFAULT 14,
  last_collected_at TIMESTAMPTZ,
  last_duration_ms INTEGER,
  last_error TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS servers_api_ping_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  enabled BOOLEAN NOT NULL DEFAULT FALSE,
  interval_sec INTEGER NOT NULL DEFAULT 120,
  last_collected_at TIMESTAMPTZ,
  last_duration_ms INTEGER,
  last_error TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS traffic_flow_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  enabled BOOLEAN NOT NULL DEFAULT FALSE,
  collector_ip TEXT NOT NULL DEFAULT '10.255.254.1',
  flow_listen_port INTEGER NOT NULL DEFAULT 4739,
  wg_listen_port INTEGER NOT NULL DEFAULT 51821,
  prefix TEXT NOT NULL DEFAULT '10.255.254.0/24',
  public_endpoint TEXT NOT NULL DEFAULT '',
  host_public_key TEXT NOT NULL DEFAULT '',
  host_private_key TEXT NOT NULL DEFAULT '',
  hub_server_id BIGINT,
  retention_hours INTEGER NOT NULL DEFAULT 24,
  top_n INTEGER NOT NULL DEFAULT 200,
  map_service_min_share_pct DOUBLE PRECISION NOT NULL DEFAULT 5,
  last_datagram_at TIMESTAMPTZ,
  last_exporter_ip TEXT,
  last_error TEXT,
  packets_received BIGINT NOT NULL DEFAULT 0,
  peers_json JSONB NOT NULL DEFAULT '[]'::jsonb,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS uptime_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  resources_enabled BOOLEAN NOT NULL DEFAULT TRUE,
  ping_enabled BOOLEAN NOT NULL DEFAULT TRUE,
  speed_enabled BOOLEAN NOT NULL DEFAULT TRUE,
  interval_sec INTEGER NOT NULL DEFAULT 15,
  probe_interval_sec INTEGER NOT NULL DEFAULT 15,
  speed_interval_sec INTEGER NOT NULL DEFAULT 60,
  retention_days INTEGER NOT NULL DEFAULT 14,
  last_collected_at TIMESTAMPTZ,
  last_duration_ms INTEGER,
  last_error TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS evobgp_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  base_url TEXT NOT NULL DEFAULT '',
  api_key TEXT NOT NULL DEFAULT '',
  enabled BOOLEAN NOT NULL DEFAULT FALSE,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS alert_telegram_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  bot_token TEXT NOT NULL DEFAULT '',
  chat_id TEXT NOT NULL DEFAULT '',
  message_thread_id INTEGER,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS acme_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  directory_url TEXT NOT NULL DEFAULT 'https://acme-v02.api.letsencrypt.org/directory',
  cloudflare_api_token TEXT NOT NULL DEFAULT '',
  default_zone_id TEXT NOT NULL DEFAULT '',
  account_private_key TEXT NOT NULL DEFAULT '',
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS certificate_renew_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  interval_sec INTEGER NOT NULL DEFAULT 21600,
  renew_before_days INTEGER NOT NULL DEFAULT 30,
  last_collected_at TIMESTAMPTZ,
  last_duration_ms INTEGER,
  last_error TEXT,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS backup_schedule_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  frequency TEXT NOT NULL DEFAULT 'daily' CHECK (frequency IN ('daily', 'weekly', 'monthly')),
  hour INTEGER NOT NULL DEFAULT 3,
  minute INTEGER NOT NULL DEFAULT 0,
  week_day INTEGER NOT NULL DEFAULT 0,
  month_day INTEGER NOT NULL DEFAULT 1,
  keep_count INTEGER NOT NULL DEFAULT 7,
  format TEXT NOT NULL DEFAULT 'rsc' CHECK (format IN ('rsc', 'backup')),
  server_ids_json JSONB NOT NULL DEFAULT '[]'::jsonb,
  last_run_at TIMESTAMPTZ,
  last_duration_ms INTEGER,
  last_error TEXT,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS internet_path_settings (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  interval_sec INTEGER NOT NULL DEFAULT 300,
  retention_days INTEGER NOT NULL DEFAULT 14,
  last_collected_at TIMESTAMPTZ,
  last_duration_ms INTEGER,
  last_error TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS alert_engine_cursor (
  id BIGINT PRIMARY KEY CHECK (id = 1),
  last_source_finished_at TIMESTAMPTZ,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS data_migration (
  id INTEGER PRIMARY KEY CHECK (id = 1),
  sqlite_imported_at TIMESTAMPTZ,
  sqlite_path TEXT,
  sqlite_sha256 TEXT,
  report_json JSONB
);

CREATE TABLE IF NOT EXISTS filter_rules (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  sort_order INTEGER NOT NULL DEFAULT 0,
  community TEXT NOT NULL,
  community_name TEXT,
  action TEXT NOT NULL DEFAULT 'route' CHECK (action IN ('route', 'blackhole')),
  gateway TEXT NOT NULL DEFAULT '',
  gateway_tunnel_id TEXT NOT NULL DEFAULT '',
  description TEXT NOT NULL DEFAULT '',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_filter_rules_server_sort ON filter_rules(server_id, sort_order);

CREATE TABLE IF NOT EXISTS recursive_routes (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  sort_order INTEGER NOT NULL DEFAULT 0,
  dst_address TEXT NOT NULL,
  gateway TEXT NOT NULL,
  distance INTEGER NOT NULL DEFAULT 1,
  scope INTEGER,
  target_scope INTEGER,
  routing_table TEXT NOT NULL DEFAULT 'main',
  check_gateway TEXT NOT NULL DEFAULT '',
  country TEXT NOT NULL DEFAULT '',
  comment TEXT NOT NULL DEFAULT '',
  disabled BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_recursive_routes_server_sort ON recursive_routes(server_id, sort_order);

CREATE TABLE IF NOT EXISTS server_snapshots (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY,
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  polled_at TIMESTAMPTZ NOT NULL,
  status TEXT NOT NULL CHECK (status IN ('online', 'offline')),
  latency_ms DOUBLE PRECISION,
  ros_version TEXT,
  board_name TEXT,
  uptime TEXT,
  cpu_load INTEGER,
  free_memory BIGINT,
  total_memory BIGINT,
  identity_name TEXT,
  raw_interfaces JSONB,
  raw_ip_addresses JSONB,
  PRIMARY KEY (id, polled_at)
) PARTITION BY RANGE (polled_at);
CREATE INDEX IF NOT EXISTS idx_server_snapshots_server_time ON server_snapshots(server_id, polled_at);
CREATE INDEX IF NOT EXISTS idx_server_snapshots_polled_brin ON server_snapshots USING BRIN (polled_at);

CREATE TABLE IF NOT EXISTS traffic_samples (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY,
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  interface_name TEXT NOT NULL,
  peer_public_key TEXT NOT NULL DEFAULT '',
  sampled_at TIMESTAMPTZ NOT NULL,
  rx_bytes BIGINT NOT NULL DEFAULT 0,
  tx_bytes BIGINT NOT NULL DEFAULT 0,
  rx_bps BIGINT NOT NULL DEFAULT 0,
  tx_bps BIGINT NOT NULL DEFAULT 0,
  running BOOLEAN NOT NULL DEFAULT FALSE,
  disabled BOOLEAN NOT NULL DEFAULT FALSE,
  PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_traffic_samples_server_time ON traffic_samples(server_id, sampled_at);
CREATE INDEX IF NOT EXISTS idx_traffic_samples_server_iface_time ON traffic_samples(server_id, interface_name, sampled_at);
CREATE INDEX IF NOT EXISTS idx_traffic_samples_sampled_brin ON traffic_samples USING BRIN (sampled_at);

CREATE TABLE IF NOT EXISTS servers_rest_ping_samples (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY,
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  sampled_at TIMESTAMPTZ NOT NULL,
  ok BOOLEAN NOT NULL,
  latency_ms INTEGER,
  error TEXT,
  PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_servers_rest_ping_samples_server_id ON servers_rest_ping_samples(server_id, sampled_at);
CREATE INDEX IF NOT EXISTS idx_servers_rest_ping_samples_brin ON servers_rest_ping_samples USING BRIN (sampled_at);

CREATE TABLE IF NOT EXISTS flow_buckets (
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  bucket_at TIMESTAMPTZ NOT NULL,
  src TEXT NOT NULL,
  dst TEXT NOT NULL,
  proto INTEGER NOT NULL DEFAULT 0,
  src_port INTEGER NOT NULL DEFAULT 0,
  dst_port INTEGER NOT NULL DEFAULT 0,
  bytes BIGINT NOT NULL DEFAULT 0,
  packets BIGINT NOT NULL DEFAULT 0,
  in_iface TEXT NOT NULL DEFAULT '',
  out_iface TEXT NOT NULL DEFAULT '',
  next_hop TEXT NOT NULL DEFAULT '',
  flow_start_ms BIGINT NOT NULL DEFAULT 0,
  flow_end_ms BIGINT NOT NULL DEFAULT 0,
  PRIMARY KEY (server_id, bucket_at, src, dst, proto, src_port, dst_port, in_iface)
) PARTITION BY RANGE (bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_buckets_server_time ON flow_buckets(server_id, bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_buckets_bucket_brin ON flow_buckets USING BRIN (bucket_at);

CREATE TABLE IF NOT EXISTS flow_minute_stats (
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  bucket_at TIMESTAMPTZ NOT NULL,
  bytes BIGINT NOT NULL DEFAULT 0,
  packets BIGINT NOT NULL DEFAULT 0,
  unique_src INTEGER NOT NULL DEFAULT 0,
  unique_dst INTEGER NOT NULL DEFAULT 0,
  conversations INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (server_id, bucket_at)
) PARTITION BY RANGE (bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_minute_stats_brin ON flow_minute_stats USING BRIN (bucket_at);

CREATE TABLE IF NOT EXISTS flow_minute_dims (
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  bucket_at TIMESTAMPTZ NOT NULL,
  dim TEXT NOT NULL,
  key TEXT NOT NULL,
  bytes BIGINT NOT NULL DEFAULT 0,
  packets BIGINT NOT NULL DEFAULT 0,
  PRIMARY KEY (server_id, bucket_at, dim, key)
) PARTITION BY RANGE (bucket_at);
CREATE INDEX IF NOT EXISTS idx_flow_minute_dims_time ON flow_minute_dims(bucket_at, dim);
CREATE INDEX IF NOT EXISTS idx_flow_minute_dims_brin ON flow_minute_dims USING BRIN (bucket_at);

CREATE TABLE IF NOT EXISTS flow_daily_dims (
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  day DATE NOT NULL,
  dim TEXT NOT NULL,
  key TEXT NOT NULL,
  bytes BIGINT NOT NULL DEFAULT 0,
  packets BIGINT NOT NULL DEFAULT 0,
  PRIMARY KEY (server_id, day, dim, key)
) PARTITION BY RANGE (day);
CREATE INDEX IF NOT EXISTS idx_flow_daily_dims_day ON flow_daily_dims(day, dim);

CREATE TABLE IF NOT EXISTS flow_ip_meta (
  prefix TEXT PRIMARY KEY,
  asn INTEGER NOT NULL DEFAULT 0,
  country TEXT NOT NULL DEFAULT '',
  lat DOUBLE PRECISION,
  lng DOUBLE PRECISION,
  holder TEXT NOT NULL DEFAULT '',
  ok INTEGER NOT NULL DEFAULT 1,
  fetched_at TIMESTAMPTZ NOT NULL
);

CREATE TABLE IF NOT EXISTS flow_asn_meta (
  asn INTEGER PRIMARY KEY,
  holder TEXT NOT NULL DEFAULT '',
  fetched_at TIMESTAMPTZ NOT NULL
);

CREATE TABLE IF NOT EXISTS uptime_probes (
  id TEXT PRIMARY KEY,
  src_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  src_interface TEXT NOT NULL DEFAULT '',
  name TEXT NOT NULL,
  target TEXT NOT NULL,
  probe_filter TEXT NOT NULL DEFAULT '—',
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  interval_sec INTEGER NOT NULL DEFAULT 0,
  show_on_dashboard BOOLEAN NOT NULL DEFAULT FALSE,
  sort_order INTEGER NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_uptime_probes_server_sort ON uptime_probes(src_server_id, sort_order);

CREATE TABLE IF NOT EXISTS uptime_probe_samples (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY,
  probe_id TEXT NOT NULL REFERENCES uptime_probes(id) ON DELETE CASCADE,
  sampled_at TIMESTAMPTZ NOT NULL,
  rtt_ms INTEGER,
  loss_pct INTEGER NOT NULL DEFAULT 0,
  status TEXT NOT NULL DEFAULT 'down' CHECK (status IN ('up', 'warn', 'down')),
  PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_uptime_probe_samples_probe_time ON uptime_probe_samples(probe_id, sampled_at);
CREATE INDEX IF NOT EXISTS idx_uptime_probe_samples_brin ON uptime_probe_samples USING BRIN (sampled_at);

CREATE TABLE IF NOT EXISTS uptime_resource_samples (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY,
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  sampled_at TIMESTAMPTZ NOT NULL,
  status TEXT NOT NULL DEFAULT 'offline' CHECK (status IN ('online', 'offline')),
  cpu_load INTEGER NOT NULL DEFAULT 0,
  free_memory BIGINT NOT NULL DEFAULT 0,
  total_memory BIGINT NOT NULL DEFAULT 0,
  free_hdd_space BIGINT NOT NULL DEFAULT 0,
  total_hdd_space BIGINT NOT NULL DEFAULT 0,
  uptime_seconds BIGINT NOT NULL DEFAULT 0,
  board_name TEXT NOT NULL DEFAULT '',
  ros_version TEXT NOT NULL DEFAULT '',
  PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_uptime_resource_samples_server_time ON uptime_resource_samples(server_id, sampled_at);
CREATE INDEX IF NOT EXISTS idx_uptime_resource_samples_brin ON uptime_resource_samples USING BRIN (sampled_at);

CREATE TABLE IF NOT EXISTS uptime_speed_probes (
  id TEXT PRIMARY KEY,
  src_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  dst_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  src_interface TEXT NOT NULL DEFAULT '',
  dst_interface TEXT NOT NULL DEFAULT '',
  protocol TEXT NOT NULL DEFAULT 'tcp' CHECK (protocol IN ('tcp', 'udp')),
  direction TEXT NOT NULL DEFAULT 'both' CHECK (direction IN ('transmit', 'receive', 'both')),
  duration_sec INTEGER NOT NULL DEFAULT 10,
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  last_run_at TIMESTAMPTZ,
  last_tx_avg_mbps DOUBLE PRECISION,
  last_rx_avg_mbps DOUBLE PRECISION,
  last_status TEXT CHECK (last_status IN ('done', 'error')),
  last_error TEXT,
  last_ping_rtt_ms INTEGER,
  last_ping_loss_pct INTEGER,
  last_ping_at TIMESTAMPTZ,
  last_ping_error TEXT,
  sort_order INTEGER NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_uptime_speed_probes_src_sort ON uptime_speed_probes(src_server_id, sort_order);

CREATE TABLE IF NOT EXISTS uptime_speed_test_runs (
  id TEXT PRIMARY KEY,
  probe_id TEXT,
  src_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  dst_server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  src_interface TEXT NOT NULL DEFAULT '',
  dst_interface TEXT NOT NULL DEFAULT '',
  src_address TEXT,
  dst_address TEXT,
  src_interface_address TEXT,
  dst_interface_address TEXT,
  protocol TEXT NOT NULL DEFAULT 'tcp' CHECK (protocol IN ('tcp', 'udp')),
  direction TEXT NOT NULL DEFAULT 'both' CHECK (direction IN ('transmit', 'receive', 'both')),
  duration_sec INTEGER NOT NULL DEFAULT 10,
  tx_avg_mbps DOUBLE PRECISION,
  rx_avg_mbps DOUBLE PRECISION,
  ping_rtt_ms INTEGER,
  ping_loss_pct INTEGER,
  ping_error TEXT,
  status TEXT NOT NULL DEFAULT 'done' CHECK (status IN ('done', 'error')),
  error TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_uptime_speed_test_runs_created_at ON uptime_speed_test_runs(created_at);

CREATE TABLE IF NOT EXISTS certificate_issue_jobs (
  id TEXT PRIMARY KEY,
  status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'running', 'done', 'failed')),
  step TEXT NOT NULL DEFAULT 'queued',
  source TEXT NOT NULL DEFAULT 'manual' CHECK (source IN ('manual', 'scheduler')),
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  cert_name TEXT NOT NULL,
  domain_names JSONB NOT NULL,
  key_type TEXT NOT NULL DEFAULT 'rsa2048',
  trust_store TEXT NOT NULL DEFAULT 'www,api',
  requested_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  started_at TIMESTAMPTZ,
  finished_at TIMESTAMPTZ,
  error TEXT
);

CREATE TABLE IF NOT EXISTS backup_entries (
  id TEXT PRIMARY KEY,
  server_id BIGINT REFERENCES servers(id) ON DELETE SET NULL,
  server_name TEXT NOT NULL,
  filename TEXT NOT NULL,
  size_bytes BIGINT NOT NULL,
  kind TEXT NOT NULL DEFAULT 'manual' CHECK (kind IN ('manual', 'auto')),
  notes TEXT,
  created_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_backup_entries_filename ON backup_entries(filename);
CREATE INDEX IF NOT EXISTS idx_backup_entries_server_created ON backup_entries(server_id, created_at);

CREATE TABLE IF NOT EXISTS alert_groups (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  combine_mode TEXT NOT NULL DEFAULT 'any' CHECK (combine_mode IN ('any', 'all')),
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  cooldown_override TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS alert_rules (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  type TEXT NOT NULL,
  target TEXT NOT NULL,
  condition TEXT NOT NULL,
  severity TEXT NOT NULL CHECK (severity IN ('critical', 'warning', 'info')),
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  cooldown TEXT NOT NULL DEFAULT '5м',
  rule_chat_id TEXT NOT NULL DEFAULT '',
  confirm_stability_sec INTEGER,
  recovery_mode TEXT NOT NULL DEFAULT 'always' CHECK (recovery_mode IN ('always', 'never', 'conditional')),
  recovery_stability_sec INTEGER,
  group_id TEXT REFERENCES alert_groups(id) ON DELETE SET NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS alert_rule_targets (
  id TEXT PRIMARY KEY,
  rule_id TEXT NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE,
  target TEXT NOT NULL,
  sort_index INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_alert_rule_targets_rule ON alert_rule_targets(rule_id);

CREATE TABLE IF NOT EXISTS alert_rule_conditions (
  id TEXT PRIMARY KEY,
  rule_id TEXT NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE,
  condition_line TEXT NOT NULL,
  sort_index INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_alert_rule_conditions_rule ON alert_rule_conditions(rule_id);

CREATE TABLE IF NOT EXISTS alert_engine_state (
  scope_key TEXT PRIMARY KEY,
  last_fired_at TEXT NOT NULL DEFAULT '',
  last_payload_hash TEXT
);

CREATE TABLE IF NOT EXISTS alert_engine_prev_live (
  kind TEXT PRIMARY KEY CHECK (kind IN ('gre', 'bgp')),
  payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS alert_engine_confirm_pending (
  rule_id TEXT PRIMARY KEY,
  payload_hash TEXT NOT NULL,
  since_at TIMESTAMPTZ NOT NULL
);

CREATE TABLE IF NOT EXISTS alert_history (
  id TEXT PRIMARY KEY,
  rule_id TEXT,
  group_id TEXT,
  rule_name TEXT NOT NULL,
  severity TEXT NOT NULL CHECK (severity IN ('critical', 'warning', 'info')),
  message TEXT NOT NULL,
  sent_ok BOOLEAN NOT NULL DEFAULT TRUE,
  fired_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_alert_history_fired_at ON alert_history(fired_at);
CREATE INDEX IF NOT EXISTS idx_alert_history_rule_id ON alert_history(rule_id);

CREATE TABLE IF NOT EXISTS alert_outbox (
  id TEXT PRIMARY KEY,
  dedupe_key TEXT NOT NULL,
  channel TEXT NOT NULL DEFAULT 'telegram' CHECK (channel IN ('telegram')),
  status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
  retry_count INTEGER NOT NULL DEFAULT 0,
  max_retries INTEGER NOT NULL DEFAULT 3,
  next_attempt_at TIMESTAMPTZ NOT NULL,
  payload_json JSONB NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  sent_at TIMESTAMPTZ,
  last_error TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_alert_outbox_dedupe ON alert_outbox(dedupe_key);
CREATE INDEX IF NOT EXISTS idx_alert_outbox_status_next_attempt ON alert_outbox(status, next_attempt_at);
CREATE INDEX IF NOT EXISTS idx_alert_outbox_pending_next ON alert_outbox(next_attempt_at) WHERE status = 'pending';

CREATE TABLE IF NOT EXISTS scheduler_runs (
  id TEXT PRIMARY KEY,
  job_key TEXT NOT NULL,
  started_at TIMESTAMPTZ NOT NULL,
  finished_at TIMESTAMPTZ NOT NULL,
  status TEXT NOT NULL CHECK (status IN ('ok', 'error')),
  error TEXT,
  duration_ms INTEGER NOT NULL DEFAULT 0,
  result_json JSONB
);
CREATE INDEX IF NOT EXISTS idx_scheduler_runs_job_time ON scheduler_runs(job_key, started_at);

CREATE TABLE IF NOT EXISTS events (
  id TEXT PRIMARY KEY,
  created_at TIMESTAMPTZ NOT NULL,
  level TEXT NOT NULL CHECK (level IN ('critical', 'warning', 'info')),
  event_type TEXT NOT NULL,
  source_module TEXT NOT NULL,
  title TEXT NOT NULL,
  message TEXT NOT NULL,
  entity_type TEXT,
  entity_id TEXT,
  payload_json JSONB
);
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_level_created_at ON events(level, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_source_created_at ON events(source_module, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_event_type_created_at ON events(event_type, created_at DESC);

CREATE TABLE IF NOT EXISTS app_users (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL DEFAULT '',
  login TEXT NOT NULL UNIQUE,
  email TEXT NOT NULL DEFAULT '',
  role TEXT NOT NULL DEFAULT 'viewer' CHECK (role IN ('admin', 'operator', 'viewer')),
  active BOOLEAN NOT NULL DEFAULT TRUE,
  avatar TEXT NOT NULL DEFAULT '',
  last_seen TIMESTAMPTZ,
  sections_json JSONB NOT NULL DEFAULT '[]'::jsonb,
  servers_json JSONB NOT NULL DEFAULT '[]'::jsonb,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS user_interface_bindings (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
  server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
  interface_name TEXT NOT NULL,
  interface_type TEXT NOT NULL DEFAULT 'other' CHECK (interface_type IN ('ether', 'gre', 'wg', 'other')),
  peer_public_key TEXT NOT NULL DEFAULT '',
  peer_name TEXT NOT NULL DEFAULT '',
  comment TEXT NOT NULL DEFAULT '',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (server_id, interface_name, peer_public_key)
);
CREATE INDEX IF NOT EXISTS idx_user_iface_bind_user ON user_interface_bindings(user_id);

CREATE TABLE IF NOT EXISTS internet_path_snapshots (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY,
  sampled_at TIMESTAMPTZ NOT NULL,
  payload_json JSONB NOT NULL,
  PRIMARY KEY (id, sampled_at)
) PARTITION BY RANGE (sampled_at);
CREATE INDEX IF NOT EXISTS idx_internet_path_snapshots_sampled ON internet_path_snapshots(sampled_at);
CREATE INDEX IF NOT EXISTS idx_internet_path_snapshots_brin ON internet_path_snapshots USING BRIN (sampled_at);

INSERT INTO traffic_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO traffic_flow_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO uptime_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO evobgp_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO servers_api_ping_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO internet_path_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO alert_telegram_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO acme_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO certificate_renew_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO backup_schedule_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO alert_engine_cursor (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
INSERT INTO data_migration (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
