//! Schema bootstrap and migration runner.
//!
//! Migrations are an ordered list of `( version, sql )` pairs. The last
//! applied version is recorded in `sync_state.schema_version`. A migration
//! that has already been applied is skipped.
//!
//! Once a migration is shipped to a user we never edit it; future schema
//! changes get a new entry with a new version number.

use rusqlite::{ Connection, OptionalExtension };

use crate::db::DbResult;

/// (version, SQL) tuples applied in order. Each migration runs in a single
/// transaction; on failure the transaction is rolled back.
const MIGRATIONS: &[ ( i64, &str ) ] = &[
	( 1, V1_INITIAL ),
	( 2, V2_CARDDAV_UID ),
	( 3, V3_CHANGE_LOG_BATCH ),
];

pub fn apply_migrations( conn: &mut Connection ) -> DbResult<()>
{
	// Bootstrap sync_state itself before we can read schema_version from it.
	conn.execute_batch(
		"CREATE TABLE IF NOT EXISTS sync_state (
			key   TEXT PRIMARY KEY,
			value TEXT NOT NULL
		);"
	)?;

	let current = current_schema_version( conn )?;
	for ( version, sql ) in MIGRATIONS
	{
		if *version > current
		{
			apply_one( conn, *version, sql )?;
		}
	}
	Ok( () )
}

fn current_schema_version( conn: &Connection ) -> DbResult<i64>
{
	let raw: Option<String> = conn.query_row(
		"SELECT value FROM sync_state WHERE key = 'schema_version'",
		[],
		|row| row.get( 0 ),
	).optional()?;
	Ok( raw.and_then( |s| s.parse::<i64>().ok() ).unwrap_or( 0 ) )
}

fn apply_one( conn: &mut Connection, version: i64, sql: &str ) -> DbResult<()>
{
	let tx = conn.transaction()?;
	tx.execute_batch( sql )?;
	tx.execute(
		"INSERT INTO sync_state( key, value ) VALUES( 'schema_version', ?1 )
		 ON CONFLICT( key ) DO UPDATE SET value = excluded.value",
		[ version.to_string() ],
	)?;
	tx.commit()?;
	Ok( () )
}

const V1_INITIAL: &str = r#"
-- v1 initial schema. See docs/schema.md for column-level documentation.

-- Aggregated contacts. Created first so raw_contacts can FK back to it; the
-- forward reference from contacts.photo_id -> data._id is allowed because
-- SQLite enforces FKs at INSERT time, not at CREATE TABLE.
CREATE TABLE contacts
(
	_id                 INTEGER PRIMARY KEY AUTOINCREMENT,
	display_name        TEXT,
	photo_id            INTEGER REFERENCES data( _id ) ON DELETE SET NULL,
	times_contacted     INTEGER NOT NULL DEFAULT 0,
	last_time_contacted INTEGER,
	starred             INTEGER NOT NULL DEFAULT 0,
	custom_ringtone     TEXT,
	send_to_voicemail   INTEGER NOT NULL DEFAULT 0,
	created_at          INTEGER NOT NULL,
	updated_at          INTEGER NOT NULL
);

CREATE INDEX idx_contacts_display_name ON contacts( display_name );
CREATE INDEX idx_contacts_starred      ON contacts( starred ) WHERE starred = 1;

-- Single-source per-person rows.
CREATE TABLE raw_contacts
(
	_id          INTEGER PRIMARY KEY AUTOINCREMENT,
	contact_id   INTEGER NOT NULL REFERENCES contacts( _id ) ON DELETE CASCADE,
	account_name TEXT,
	account_type TEXT,
	display_name TEXT,
	deleted      INTEGER NOT NULL DEFAULT 0,
	dirty        INTEGER NOT NULL DEFAULT 0,
	version      INTEGER NOT NULL DEFAULT 1,
	sourceid     TEXT,
	created_at   INTEGER NOT NULL,
	updated_at   INTEGER NOT NULL
);

CREATE INDEX idx_raw_contacts_contact_id ON raw_contacts( contact_id );
CREATE INDEX idx_raw_contacts_account    ON raw_contacts( account_type, account_name );
CREATE INDEX idx_raw_contacts_dirty      ON raw_contacts( dirty ) WHERE dirty = 1;

-- Per-fact rows (phones, emails, names, photos, ...).
CREATE TABLE data
(
	_id              INTEGER PRIMARY KEY AUTOINCREMENT,
	raw_contact_id   INTEGER NOT NULL REFERENCES raw_contacts( _id ) ON DELETE CASCADE,
	mimetype         TEXT    NOT NULL,
	data1            TEXT,
	data2            TEXT,
	data3            TEXT,
	data4            TEXT,
	data5            TEXT,
	data6            TEXT,
	data7            TEXT,
	data8            TEXT,
	data9            TEXT,
	data10           TEXT,
	data11           TEXT,
	data12           TEXT,
	data13           TEXT,
	data14           TEXT,
	data15           BLOB,
	is_primary       INTEGER NOT NULL DEFAULT 0,
	is_super_primary INTEGER NOT NULL DEFAULT 0,
	data_version     INTEGER NOT NULL DEFAULT 1,
	sync1            TEXT,
	sync2            TEXT,
	sync3            TEXT,
	sync4            TEXT,
	created_at       INTEGER NOT NULL,
	updated_at       INTEGER NOT NULL
);

CREATE INDEX idx_data_raw_contact  ON data( raw_contact_id );
CREATE INDEX idx_data_mimetype     ON data( mimetype );
CREATE INDEX idx_data_phone_search ON data( data4 ) WHERE mimetype = 'vnd.android.cursor.item/phone_v2';
CREATE INDEX idx_data_email_search ON data( data1 ) WHERE mimetype = 'vnd.android.cursor.item/email_v2';

-- Groups (label / category).
CREATE TABLE groups
(
	_id        INTEGER PRIMARY KEY AUTOINCREMENT,
	title      TEXT    NOT NULL,
	notes      TEXT,
	system_id  TEXT,
	deleted    INTEGER NOT NULL DEFAULT 0,
	created_at INTEGER NOT NULL,
	updated_at INTEGER NOT NULL
);

CREATE TABLE group_membership
(
	_id            INTEGER PRIMARY KEY AUTOINCREMENT,
	raw_contact_id INTEGER NOT NULL REFERENCES raw_contacts( _id ) ON DELETE CASCADE,
	group_id       INTEGER NOT NULL REFERENCES groups( _id )       ON DELETE CASCADE,
	UNIQUE( raw_contact_id, group_id )
);

CREATE INDEX idx_group_membership_raw   ON group_membership( raw_contact_id );
CREATE INDEX idx_group_membership_group ON group_membership( group_id );

-- Append-only change log feeding CardDAV sync-collection.
CREATE TABLE change_log
(
	counter        INTEGER PRIMARY KEY,
	raw_contact_id INTEGER NOT NULL,
	change_kind    TEXT    NOT NULL,
	happened_at    INTEGER NOT NULL
);

-- Bootstrap sync_state keys we want present from migration 0.
INSERT INTO sync_state( key, value ) VALUES
	( 'change_counter', '0' );
"#;

const V2_CARDDAV_UID: &str = r#"
-- v2: dedicated CardDAV resource id (UUIDv4) per raw_contact, decoupled
-- from `sourceid` which is reserved for upstream sync engine ids. The
-- column is nullable so we can populate it lazily on first request — see
-- db::raw_contacts::ensure_carddav_uid.

ALTER TABLE raw_contacts ADD COLUMN carddav_uid TEXT;

CREATE UNIQUE INDEX idx_raw_contacts_carddav_uid
	ON raw_contacts( carddav_uid )
	WHERE carddav_uid IS NOT NULL;
"#;

const V3_CHANGE_LOG_BATCH: &str = r#"
-- v3: ApplyBatch needs to record many change_log rows under the same
-- counter value (one batch == one counter bump, but every affected
-- raw_contact still gets its own log entry so sync-collection can list
-- them individually). The original schema had counter as PRIMARY KEY
-- which forbids that — replace it with an auto-id and an index on
-- counter for the sync-collection scan.

CREATE TABLE change_log_v3
(
	_id            INTEGER PRIMARY KEY AUTOINCREMENT,
	counter        INTEGER NOT NULL,
	raw_contact_id INTEGER NOT NULL,
	change_kind    TEXT    NOT NULL,
	happened_at    INTEGER NOT NULL
);

INSERT INTO change_log_v3 ( counter, raw_contact_id, change_kind, happened_at )
	SELECT counter, raw_contact_id, change_kind, happened_at
	FROM change_log;

DROP TABLE change_log;
ALTER TABLE change_log_v3 RENAME TO change_log;

CREATE INDEX idx_change_log_counter ON change_log( counter );
"#;
