package storage

import (
	"database/sql"
	"fmt"
	"log"
)

var migrations = []func(*sql.Tx)error{
	m01_initial,
	m02_feed_states_and_errors,
	m03_on_delete_actions,
	m04_item_podcasturl,
}

var maxVersion = int64(len(migrations))

func migrate(db *sql.DB, log *log.Logger) error {
	var version int64
	db.QueryRow("pragma user_version").Scan(&version);

	if version >= maxVersion {
		return nil
	}

	log.Printf("db version is %d. migrating to %d", version, maxVersion)

	for v := version + 1; v <= maxVersion; v++ {
		// Migrations altering schema using a sequence of steps due to SQLite limitations.
		// Must come with `pragma foreign_key_check` at the end. See:
		// "Making Other Kinds Of Table Schema Changes"
	    // https://www.sqlite.org/lang_altertable.html
		trickyAlteration := (v == 3)

		log.Printf("[migration:%d] starting", v)

		if trickyAlteration {
			db.Exec("pragma foreign_keys=off;")
		}

		err := migrateVersion(v, db)

		if trickyAlteration {
			db.Exec("pragma foreign_keys=on;")
		}

		if err != nil {
			return err
		}

		log.Printf("[migration:%d] done", v)
	}
	return nil
}

func migrateVersion(v int64, db *sql.DB) error {
	var err error
	var tx *sql.Tx
	migratefunc := migrations[v - 1]
	if tx, err = db.Begin(); err != nil {
		log.Printf("[migration:%d] failed to start transaction", v)
		return err
	}
	if err = migratefunc(tx); err != nil {
		log.Printf("[migration:%d] failed to migrate", v)
		tx.Rollback()
		return err
	}
	if _, err = tx.Exec(fmt.Sprintf("pragma user_version = %d", v)); err != nil {
		log.Printf("[migration:%d] failed to bump version", v)
		tx.Rollback()
		return err
	}
	if err = tx.Commit(); err != nil {
		log.Printf("[migration:%d] failed to commit changes", v)
		return err
	}
	return nil
}

func m01_initial(tx *sql.Tx) error {
	sql := `
		create table if not exists folders (
		 id             integer primary key autoincrement,
		 title          text not null,
		 is_expanded    boolean not null default false
		);

		create unique index if not exists idx_folder_title on folders(title);

		create table if not exists feeds (
		 id             integer primary key autoincrement,
		 folder_id      references folders(id),
		 title          text not null,
		 description    text,
		 link           text,
		 feed_link      text not null,
		 icon           blob
		);

		create index if not exists idx_feed_folder_id on feeds(folder_id);
		create unique index if not exists idx_feed_feed_link on feeds(feed_link);

		create table if not exists items (
		 id             integer primary key autoincrement,
		 guid           string not null,
		 feed_id        references feeds(id),
		 title          text,
		 link           text,
		 description    text,
		 content        text,
		 author         text,
		 date           datetime,
		 date_updated   datetime,
		 date_arrived   datetime,
		 status         integer,
		 image          text,
		 search_rowid   integer
		);

		create index if not exists idx_item_feed_id on items(feed_id);
		create index if not exists idx_item_status  on items(status);
		create index if not exists idx_item_search_rowid on items(search_rowid);
		create unique index if not exists idx_item_guid on items(feed_id, guid);

		create table if not exists settings (
		 key            string primary key,
		 val            blob
		);

		create virtual table if not exists search using fts4(title, description, content);

		create trigger if not exists del_item_search after delete on items begin
		  delete from search where rowid = old.search_rowid;
		end;
	`
	_, err := tx.Exec(sql)
	return err
}

func m02_feed_states_and_errors(tx *sql.Tx) error {
	sql := `
		create table if not exists http_states (
		 feed_id        references feeds(id) unique,
		 last_refreshed datetime not null,

		 -- http header fields --
		 last_modified  string not null,
		 etag           string not null
		);

		create table if not exists feed_errors (
		 feed_id        references feeds(id) unique,
		 error          string
		);
	`
	_, err := tx.Exec(sql)
	return err
}

func m03_on_delete_actions(tx *sql.Tx) error {
	sql := `
		-- 01. create altered tables
		create table if not exists new_feeds (
		 id             integer primary key autoincrement,
		 folder_id      references folders(id) on delete set null,
		 title          text not null,
		 description    text,
		 link           text,
		 feed_link      text not null,
		 icon           blob
		);
		create table if not exists new_items (
		 id             integer primary key autoincrement,
		 guid           string not null,
		 feed_id        references feeds(id) on delete cascade,
		 title          text,
		 link           text,
		 description    text,
		 content        text,
		 author         text,
		 date           datetime,
		 date_updated   datetime,
		 date_arrived   datetime,
		 status         integer,
		 image          text,
		 search_rowid   integer
		);
		create table if not exists new_http_states (
		 feed_id        references feeds(id) on delete cascade unique,
		 last_refreshed datetime not null,
		 last_modified  string not null,
		 etag           string not null
		);
		create table if not exists new_feed_errors (
		 feed_id        references feeds(id) on delete cascade unique,
		 error          string
		);

		-- 02. transfer data into new tables
		insert into new_feeds select * from feeds;
		insert into new_items select * from items;
		insert into new_http_states select * from http_states;
		insert into new_feed_errors select * from feed_errors;

		-- 03. drop old tables
		drop table feeds;
		drop table items;
		drop table http_states;
		drop table feed_errors;

		-- 04. rename new tables
		alter table new_feeds rename to feeds;
		alter table new_items rename to items;
		alter table new_http_states rename to http_states;
		alter table new_feed_errors rename to feed_errors;

		-- 05. reconstruct indexes & triggers
		create index if not exists idx_feed_folder_id on feeds(folder_id);
		create unique index if not exists idx_feed_feed_link on feeds(feed_link);
		create index if not exists idx_item_feed_id on items(feed_id);
		create index if not exists idx_item_status  on items(status);
		create index if not exists idx_item_search_rowid on items(search_rowid);
		create unique index if not exists idx_item_guid on items(feed_id, guid);
		create trigger if not exists del_item_search after delete on items begin
		  delete from search where rowid = old.search_rowid;
		end;

		-- 06. check consistency
		pragma foreign_key_check;
	`
	_, err := tx.Exec(sql)
	return err
}

func m04_item_podcasturl(tx *sql.Tx) error {
	sql := `
		alter table items add column podcast_url text
	`
	_, err := tx.Exec(sql)
	return err
}
