-- psysonic-library v1 schema — see implementation-spec.ru.md §5.1–§5.3
-- Tables are ordered so FK targets exist before their referrers.

-- Migration-runner bookkeeping (§5.7). Defined here so the schema file is
-- self-describing — `LibraryStore::run_migrations` also creates this table
-- defensively before applying migrations, which keeps both paths idempotent.
CREATE TABLE IF NOT EXISTS schema_migrations (
  version    INTEGER PRIMARY KEY,
  applied_at INTEGER NOT NULL
);

CREATE TABLE canonical_track (
  id          TEXT PRIMARY KEY,
  created_at  INTEGER NOT NULL,
  updated_at  INTEGER NOT NULL
);

CREATE TABLE canonical_identity (
  canonical_id  TEXT NOT NULL,
  kind          TEXT NOT NULL,
  value         TEXT NOT NULL,
  confidence    REAL NOT NULL DEFAULT 1.0,
  PRIMARY KEY (kind, value),
  FOREIGN KEY (canonical_id) REFERENCES canonical_track(id)
);

CREATE TABLE sync_state (
  server_id                TEXT NOT NULL,
  library_scope            TEXT NOT NULL DEFAULT '',
  normalized_base_url      TEXT NOT NULL DEFAULT '',
  server_fingerprint_ok    INTEGER,
  fingerprint_checked_at   INTEGER,
  capability_flags         INTEGER NOT NULL DEFAULT 0,
  last_full_sync_at        INTEGER,
  last_delta_sync_at       INTEGER,
  server_last_scan_iso     TEXT,
  indexes_last_modified_ms INTEGER,
  artists_last_modified_ms INTEGER,
  server_track_count       INTEGER,
  local_track_count        INTEGER,
  artist_count             INTEGER,
  library_tier             TEXT NOT NULL DEFAULT 'unknown',
  poll_stats_json          TEXT NOT NULL DEFAULT '{}',
  next_poll_at             INTEGER,
  initial_sync_cursor_json TEXT NOT NULL DEFAULT '{}',
  sync_phase               TEXT NOT NULL DEFAULT 'idle',
  last_error               TEXT,
  n1_bulk_unreliable       INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (server_id, library_scope)
);

CREATE TABLE artist (
  server_id    TEXT NOT NULL,
  id           TEXT NOT NULL,
  name         TEXT NOT NULL,
  album_count  INTEGER,
  synced_at    INTEGER NOT NULL,
  raw_json     TEXT,
  PRIMARY KEY (server_id, id)
);

CREATE TABLE album (
  server_id     TEXT NOT NULL,
  id            TEXT NOT NULL,
  name          TEXT NOT NULL,
  artist        TEXT,
  artist_id     TEXT,
  song_count    INTEGER,
  duration_sec  INTEGER,
  year          INTEGER,
  genre         TEXT,
  cover_art_id  TEXT,
  starred_at    INTEGER,
  synced_at     INTEGER NOT NULL,
  raw_json      TEXT,
  PRIMARY KEY (server_id, id)
);

CREATE TABLE track (
  server_id            TEXT NOT NULL,
  id                   TEXT NOT NULL,
  title                TEXT NOT NULL,
  title_sort           TEXT,
  artist               TEXT,
  artist_id            TEXT,
  album                TEXT NOT NULL DEFAULT '',
  album_id             TEXT,
  album_artist         TEXT,
  duration_sec         INTEGER NOT NULL DEFAULT 0,
  track_number         INTEGER,
  disc_number          INTEGER,
  year                 INTEGER,
  genre                TEXT,
  suffix               TEXT,
  bit_rate             INTEGER,
  size_bytes           INTEGER,
  cover_art_id         TEXT,
  starred_at           INTEGER,
  user_rating          INTEGER,
  play_count           INTEGER,
  played_at            INTEGER,
  server_path          TEXT,
  library_id           TEXT,
  isrc                 TEXT,
  mbid_recording       TEXT,
  bpm                  INTEGER,
  replay_gain_track_db REAL,
  replay_gain_album_db REAL,
  content_hash         TEXT,
  server_updated_at    INTEGER,
  server_created_at    INTEGER,
  resync_gen           INTEGER NOT NULL DEFAULT 0,
  deleted              INTEGER NOT NULL DEFAULT 0,
  synced_at            INTEGER NOT NULL,
  raw_json             TEXT NOT NULL,
  PRIMARY KEY (server_id, id)
);

CREATE VIRTUAL TABLE track_fts USING fts5(
  title, artist, album, album_artist, genre,
  content='track', content_rowid='rowid',
  tokenize='unicode61 remove_diacritics 2'
);

CREATE TRIGGER track_ai AFTER INSERT ON track BEGIN
  INSERT INTO track_fts(rowid, title, artist, album, album_artist, genre)
  VALUES (new.rowid, new.title, new.artist, new.album, new.album_artist, new.genre);
END;

CREATE TRIGGER track_ad AFTER DELETE ON track BEGIN
  INSERT INTO track_fts(track_fts, rowid, title, artist, album, album_artist, genre)
  VALUES ('delete', old.rowid, old.title, old.artist, old.album, old.album_artist, old.genre);
END;

CREATE TRIGGER track_au AFTER UPDATE ON track BEGIN
  INSERT INTO track_fts(track_fts, rowid, title, artist, album, album_artist, genre)
  VALUES ('delete', old.rowid, old.title, old.artist, old.album, old.album_artist, old.genre);
  INSERT INTO track_fts(rowid, title, artist, album, album_artist, genre)
  VALUES (new.rowid, new.title, new.artist, new.album, new.album_artist, new.genre);
END;

CREATE TABLE track_extension (
  server_id   TEXT NOT NULL,
  track_id    TEXT NOT NULL,
  kind        TEXT NOT NULL,
  version     INTEGER NOT NULL DEFAULT 1,
  payload     BLOB NOT NULL,
  updated_at  INTEGER NOT NULL,
  PRIMARY KEY (server_id, track_id, kind),
  FOREIGN KEY (server_id, track_id) REFERENCES track(server_id, id)
);

-- NO FK to track: survives library purge when user keeps the cached file (§5.14).
CREATE TABLE track_offline (
  server_id         TEXT NOT NULL,
  track_id          TEXT NOT NULL,
  local_path        TEXT NOT NULL,
  file_size_bytes   INTEGER,
  suffix            TEXT,
  content_hash      TEXT NOT NULL DEFAULT '',
  server_path       TEXT,
  cached_at         INTEGER NOT NULL,
  last_verified_at  INTEGER,
  PRIMARY KEY (server_id, track_id)
);

CREATE TABLE track_id_history (
  server_id    TEXT NOT NULL,
  old_id       TEXT NOT NULL,
  new_id       TEXT NOT NULL,
  content_hash TEXT,
  server_path  TEXT,
  remapped_at  INTEGER NOT NULL,
  PRIMARY KEY (server_id, old_id)
);

CREATE TABLE track_fact (
  server_id     TEXT NOT NULL,
  track_id      TEXT NOT NULL,
  fact_kind     TEXT NOT NULL,
  value_real    REAL,
  value_int     INTEGER,
  value_text    TEXT,
  unit          TEXT,
  source_kind   TEXT NOT NULL,
  source_id     TEXT NOT NULL,
  source_detail TEXT,
  confidence    REAL NOT NULL DEFAULT 1.0,
  content_hash  TEXT,
  fetched_at    INTEGER NOT NULL,
  expires_at    INTEGER,
  PRIMARY KEY (server_id, track_id, fact_kind, source_kind, source_id),
  FOREIGN KEY (server_id, track_id) REFERENCES track(server_id, id)
);

CREATE TABLE track_artifact (
  server_id     TEXT NOT NULL,
  track_id      TEXT NOT NULL,
  artifact_kind TEXT NOT NULL,
  format        TEXT NOT NULL,
  language      TEXT,
  source_kind   TEXT NOT NULL,
  source_id     TEXT NOT NULL,
  content_text  TEXT,
  content_blob  BLOB,
  content_bytes INTEGER NOT NULL DEFAULT 0,
  not_found     INTEGER NOT NULL DEFAULT 0,
  content_hash  TEXT,
  fetched_at    INTEGER NOT NULL,
  expires_at    INTEGER,
  PRIMARY KEY (server_id, track_id, artifact_kind, source_kind, source_id, format),
  FOREIGN KEY (server_id, track_id) REFERENCES track(server_id, id)
);

CREATE TABLE track_canonical_link (
  server_id     TEXT NOT NULL,
  track_id      TEXT NOT NULL,
  canonical_id  TEXT NOT NULL,
  match_method  TEXT NOT NULL,
  confidence    REAL NOT NULL,
  linked_at     INTEGER NOT NULL,
  PRIMARY KEY (server_id, track_id),
  FOREIGN KEY (server_id, track_id) REFERENCES track(server_id, id),
  FOREIGN KEY (canonical_id) REFERENCES canonical_track(id)
);

CREATE TABLE canonical_enrichment_link (
  canonical_id    TEXT NOT NULL,
  enrichment_kind TEXT NOT NULL,
  owner_server_id TEXT NOT NULL,
  owner_track_id  TEXT NOT NULL,
  share_policy    TEXT NOT NULL DEFAULT 'isrc_match',
  linked_at       INTEGER NOT NULL,
  PRIMARY KEY (canonical_id, enrichment_kind, owner_server_id, owner_track_id),
  FOREIGN KEY (canonical_id) REFERENCES canonical_track(id)
);

CREATE TABLE play_session (
  id               INTEGER PRIMARY KEY AUTOINCREMENT,
  server_id        TEXT NOT NULL,
  track_id         TEXT NOT NULL,
  started_at_ms    INTEGER NOT NULL,
  listened_sec     REAL NOT NULL,
  position_max_sec REAL NOT NULL,
  completion       TEXT NOT NULL,
  end_reason       TEXT NOT NULL,
  FOREIGN KEY (server_id, track_id) REFERENCES track(server_id, id),
  CHECK (completion IN ('partial', 'full'))
);

CREATE TABLE track_genre (
  server_id  TEXT NOT NULL,
  track_id   TEXT NOT NULL,
  genre      TEXT NOT NULL,
  album_id   TEXT,
  library_id TEXT,
  PRIMARY KEY (server_id, track_id, genre COLLATE NOCASE),
  FOREIGN KEY (server_id, track_id) REFERENCES track(server_id, id) ON DELETE CASCADE
);

CREATE TABLE library_data_migration (
  id            TEXT PRIMARY KEY,
  cursor_rowid  INTEGER NOT NULL DEFAULT 0,
  completed_at  INTEGER,
  started_at    INTEGER
);

CREATE INDEX idx_track_album   ON track(server_id, album_id)               WHERE deleted = 0;
CREATE INDEX idx_track_artist  ON track(server_id, artist_id)              WHERE deleted = 0;
CREATE INDEX idx_track_updated ON track(server_id, server_updated_at DESC) WHERE deleted = 0;
CREATE INDEX idx_track_starred ON track(server_id, starred_at)             WHERE deleted = 0 AND starred_at IS NOT NULL;
CREATE INDEX idx_track_library ON track(server_id, library_id)             WHERE deleted = 0;
CREATE INDEX idx_track_bpm     ON track(server_id, bpm)                    WHERE deleted = 0 AND bpm IS NOT NULL;
CREATE INDEX idx_track_isrc    ON track(isrc)                              WHERE deleted = 0 AND isrc IS NOT NULL;
CREATE INDEX idx_track_fact_lookup     ON track_fact(server_id, track_id, fact_kind);
CREATE INDEX idx_track_artifact_lookup ON track_artifact(server_id, track_id, artifact_kind);
CREATE INDEX idx_track_offline_hash    ON track_offline(server_id, content_hash);
CREATE INDEX idx_track_id_history_new  ON track_id_history(server_id, new_id);
CREATE INDEX idx_canonical_identity_lookup ON canonical_identity(kind, value);

CREATE INDEX idx_track_remap_path
  ON track(server_id, server_path)
  WHERE deleted = 0 AND server_path IS NOT NULL AND server_path != '';

CREATE INDEX idx_track_remap_hash
  ON track(server_id, content_hash)
  WHERE deleted = 0 AND content_hash IS NOT NULL AND content_hash != '';

CREATE INDEX idx_track_title
  ON track(server_id, title COLLATE NOCASE)
  WHERE deleted = 0;

CREATE INDEX idx_track_genre
  ON track(server_id, genre COLLATE NOCASE)
  WHERE deleted = 0 AND genre IS NOT NULL;

CREATE INDEX idx_track_year
  ON track(server_id, year)
  WHERE deleted = 0 AND year IS NOT NULL;

CREATE INDEX idx_play_session_server_time
  ON play_session(server_id, started_at_ms DESC);

CREATE INDEX idx_play_session_track
  ON play_session(server_id, track_id, started_at_ms DESC);

CREATE INDEX idx_play_session_started
  ON play_session(started_at_ms DESC);

CREATE INDEX idx_track_fact_mood_tag
  ON track_fact(server_id, fact_kind, value_text, track_id)
  WHERE fact_kind = 'mood_tag';

CREATE INDEX idx_track_genre_browse
  ON track_genre(server_id, genre COLLATE NOCASE, album_id, track_id)
  WHERE album_id IS NOT NULL AND album_id != '';
