// Copyright (C) 2019-2026 Chrystian Huot <chrystian.huot@saubeo.solutions>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>
//
// WebSocket API Access Policy:
// This WebSocket API is reserved exclusively for Saubeo Solutions and its native applications.
// Unauthorized access is strictly prohibited.
// See API_ACCESS_POLICY.md for full terms.

package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"sort"
	"strings"
)

func migrateAccesses(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		accessId   sql.NullInt64
		code       sql.NullString
		expiration sql.NullTime
		ident      sql.NullString
		limit      sql.NullInt32
		order      sql.NullInt32
		systems    sql.NullString
	)

	formatError := errorFormatter("migration", "migrateAccesses")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerAccesses"`); err != nil {
		return nil
	}

	log.Println("migrating accesses...")

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "code", "expiration", "ident", "limit", "order", "systems" FROM "rdioScannerAccesses"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		access := NewAccess()

		if err = rows.Scan(&accessId, &code, &expiration, &ident, &limit, &order, &systems); err != nil {
			continue
		}

		if accessId.Valid {
			access.Id = uint64(accessId.Int64)
		} else {
			continue
		}

		if code.Valid && len(code.String) > 0 {
			access.Code = escapeQuotes(code.String)
		} else {
			continue
		}

		if expiration.Valid {
			access.Expiration = uint64(expiration.Time.Unix())
		}

		if ident.Valid {
			access.Ident = escapeQuotes(ident.String)
		}

		if limit.Valid {
			access.Limit = uint(limit.Int32)
		}

		if order.Valid {
			access.Order = uint(order.Int32)
		}

		if systems.Valid {
			access.Systems = systems.String
		}

		query = fmt.Sprintf(`INSERT INTO "accesses" ("accessId", "code", "expiration", "ident", "limit", "order", "systems") VALUES (%d, '%s', %d, '%s', %d, %d, '%s')`, access.Id, access.Code, access.Expiration, access.Ident, access.Limit, access.Order, access.Systems)
		if _, err = tx.Exec(query); err != nil {
			log.Println(formatError(err, query))
		}
	}

	rows.Close()

	query = `DROP TABLE "rdioScannerAccesses"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateApikeys(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		apikeyId sql.NullInt64
		disabled sql.NullBool
		ident    sql.NullString
		key      sql.NullString
		order    sql.NullInt32
		systems  sql.NullString
	)

	formatError := errorFormatter("migration", "migrateApikeys")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerApiKeys"`); err != nil {
		return nil
	}

	log.Println("migrating apikeys...")

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "disabled", "ident", "key", "order", "systems" FROM "rdioScannerApiKeys"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		apikey := NewApikey()

		if err = rows.Scan(&apikeyId, &disabled, &ident, &key, &order, &systems); err != nil {
			continue
		}

		if apikeyId.Valid {
			apikey.Id = uint64(apikeyId.Int64)
		} else {
			continue
		}

		if disabled.Valid {
			apikey.Disabled = disabled.Bool
		}

		if ident.Valid {
			apikey.Ident = escapeQuotes(ident.String)
		}

		if key.Valid {
			apikey.Key = escapeQuotes(key.String)
		}

		if order.Valid {
			apikey.Order = uint(order.Int32)
		}

		if systems.Valid {
			apikey.Systems = systems.String
		}

		query = fmt.Sprintf(`INSERT INTO "apikeys" ("apikeyId", "disabled", "ident", "key", "order", "systems") VALUES (%d, %t, '%s', '%s', %d, '%s')`, apikey.Id, apikey.Disabled, apikey.Ident, apikey.Key, apikey.Order, apikey.Systems)
		if _, err = tx.Exec(query); err != nil {
			log.Println(formatError(err, query))
		}
	}

	rows.Close()

	query = `DROP TABLE "rdioScannerApiKeys"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateCalls(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		systems    = map[int32]int32{}
		talkgroups = map[int32]map[int32]int32{}

		timestamp int64

		callId        sql.NullInt32
		audio         sql.NullString
		audioFilename sql.NullString
		audioMime     sql.NullString
		dateTime      sql.NullTime
		frequencies   sql.NullString
		frequency     sql.NullInt32
		patches       sql.NullString
		source        sql.NullInt32
		sources       sql.NullString
		systemId      sql.NullInt32
		systemRef     sql.NullInt32
		talkgroupId   sql.NullInt32
		talkgroupRef  sql.NullInt32
	)

	formatError := errorFormatter("migration", "migrateCalls")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerCalls"`); err != nil {
		return nil
	}

	log.Println("migrating calls...")

	query = `SELECT s."systemId", s."systemRef", t."talkgroupId", t."talkgroupRef" FROM "systems" AS s LEFT JOIN "talkgroups" AS t`
	if rows, err = db.Sql.Query(query); err != nil {
		return formatError(err, query)
	}

	for rows.Next() {
		if err = rows.Scan(&systemId, &systemRef, &talkgroupId, &talkgroupRef); err != nil {
			continue
		}

		if systemId.Valid && systemRef.Valid && talkgroupId.Valid && talkgroupRef.Valid {
			if systems[systemRef.Int32] == 0 {
				systems[systemRef.Int32] = systemId.Int32
				talkgroups[systemRef.Int32] = map[int32]int32{}
			}

			talkgroups[systemRef.Int32][talkgroupRef.Int32] = talkgroupId.Int32
		}
	}

	rows.Close()

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "id", "audio", "audioName", "audioType", "dateTime", "frequencies", "frequency", "patches", "source", "sources", "system", "talkgroup" FROM "rdioScannerCalls"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		call := NewCall()

		if err = rows.Scan(&callId, &audio, &audioFilename, &audioMime, &dateTime, &frequencies, &frequency, &patches, &source, &sources, &systemRef, &talkgroupRef); err != nil {
			continue
		}

		if callId.Valid {
			call.Id = uint64(callId.Int32)
		} else {
			continue
		}

		if audio.Valid && len(audio.String) > 0 {
			call.Audio = []byte(audio.String)
		} else {
			continue
		}

		if audioFilename.Valid {
			call.AudioFilename = escapeQuotes(audioFilename.String)
		}

		if audioMime.Valid {
			call.AudioMime = audioMime.String
		}

		if dateTime.Valid {
			timestamp = dateTime.Time.UnixMilli()
		} else {
			continue
		}

		if !systemRef.Valid || systems[systemRef.Int32] == 0 {
			continue
		}

		if !talkgroupRef.Valid || talkgroups[systemRef.Int32][talkgroupRef.Int32] == 0 {
			continue
		}

		if db.Config.DbType == DbTypePostgresql {
			query = fmt.Sprintf(`INSERT INTO "calls" ("callId", "audio", "audioFilename", "audioMime", "siteRef", "systemId", "talkgroupId", "timestamp") VALUES (%d, $1, '%s', '%s', 0, %d, %d, %d)`, call.Id, call.AudioFilename, call.AudioMime, systems[systemRef.Int32], talkgroups[systemRef.Int32][talkgroupRef.Int32], timestamp)

		} else {
			query = fmt.Sprintf(`INSERT INTO "calls" ("callId", "audio", "audioFilename", "audioMime", "siteRef", "systemId", "talkgroupId", "timestamp") VALUES (%d, ?, '%s', '%s', 0, %d, %d, %d)`, call.Id, call.AudioFilename, call.AudioMime, systems[systemRef.Int32], talkgroups[systemRef.Int32][talkgroupRef.Int32], timestamp)
		}

		if _, err = tx.Exec(query, call.Audio); err == nil {
			if frequencies.Valid && len(frequencies.String) > 0 {
				var f any
				if err = json.Unmarshal([]byte(frequencies.String), &f); err == nil {
					switch v := f.(type) {
					case []any:
						for _, v := range v {
							switch m := v.(type) {
							case map[string]any:
								var (
									errorCount uint
									freq       uint
									pos        float64
									spikeCount uint
								)

								switch v := m["errorCount"].(type) {
								case float64:
									errorCount = uint(v)
								}

								switch v := m["freq"].(type) {
								case float64:
									freq = uint(v)
								}

								switch v := m["pos"].(type) {
								case float64:
									pos = v
								}

								switch v := m["spikeCount"].(type) {
								case float64:
									spikeCount = uint(v)
								}

								query = fmt.Sprintf(`INSERT INTO "callFrequencies" ("callId", "errors", "frequency", "offset", "spikes") VALUES (%d, %d, %d, %f, %d)`, call.Id, errorCount, freq, pos, spikeCount)
								if _, err = tx.Exec(query); err != nil {
									log.Println(formatError(err, query))
								}
							}
						}
					}
				}

			} else if frequency.Valid && frequency.Int32 > 0 {
				query = fmt.Sprintf(`INSERT INTO "callFrequencies" ("callId", "errors", "frequency", "offset", "spikes") VALUES (%d, 0, %d, 0, 0)`, call.Id, frequency.Int32)
				if _, err = tx.Exec(query); err != nil {
					log.Println(formatError(err, query))
				}
			}

			if patches.Valid && len(patches.String) > 0 {
				var f any
				if err = json.Unmarshal([]byte(patches.String), &f); err == nil {
					switch v := f.(type) {
					case []any:
						for _, v := range v {
							switch i := v.(type) {
							case float64:
								if i := talkgroups[systemRef.Int32][int32(i)]; i > 0 {
									query = fmt.Sprintf(`INSERT INTO "callPatches" ("callId", "talkgroupId") VALUES (%d, %d)`, call.Id, i)
									if _, err = tx.Exec(query); err != nil {
										log.Println(formatError(err, query))
									}
								}
							}
						}
					}
				}
			}

			if sources.Valid && len(sources.String) > 0 && sources.String != "[]" {
				var f any
				if err = json.Unmarshal([]byte(sources.String), &f); err == nil {
					switch v := f.(type) {
					case []any:
						for _, v := range v {
							switch m := v.(type) {
							case map[string]any:
								switch src := (m["src"]).(type) {
								case float64:
									if src > 0 {
										query = fmt.Sprintf(`INSERT INTO "callUnits" ("callId", "offset", "unitRef") VALUES (%d, %f, %f)`, call.Id, m["pos"], src)
										if _, err = tx.Exec(query); err != nil {
											log.Println(formatError(err, query))
										}
									}
								}
							}
						}
					}
				}

			} else if source.Valid && source.Int32 > 0 {
				var c int
				query = fmt.Sprintf(`SELECT COUNT(*) FROM "units" WHERE "systemId" = %d AND "unitRef" = %d`, systems[systemRef.Int32], source.Int32)
				if err = tx.QueryRow(query).Scan(&c); err == nil && c == 0 {
					query = fmt.Sprintf(`INSERT INTO "units" ("label", "systemId", "unitRef") VALUES(%d, %d, %d)`, source.Int32, systems[systemRef.Int32], source.Int32)

					if err == nil {
						query = fmt.Sprintf(`INSERT INTO "callUnits" ("callId", "offset", "unitRef") VALUES (%d, %d, %d)`, call.Id, 0, source.Int32)
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}

					} else {
						log.Println(formatError(err, query))
					}

				} else if err != nil {
					log.Println(formatError(err, query))
				}
			}

		} else {
			log.Println(formatError(err, query))
		}
	}

	rows.Close()

	query = `DROP TABLE "rdioScannerCalls"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateDirwatches(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		systems    = map[int32]int32{}
		talkgroups = map[int32]map[int32]int32{}

		refSystem    any
		refTalkgroup any

		delay        sql.NullInt32
		deleteAfter  sql.NullBool
		directory    sql.NullString
		dirwatchId   sql.NullInt64
		disabled     sql.NullBool
		extension    sql.NullString
		frequency    sql.NullInt32
		kind         sql.NullString
		mask         sql.NullString
		order        sql.NullInt32
		systemId     sql.NullInt32
		systemRef    sql.NullInt32
		talkgroupId  sql.NullInt32
		talkgroupRef sql.NullInt32
	)

	formatError := errorFormatter("migration", "migrateDirwatches")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerDirwatches"`); err != nil {
		return nil
	}

	log.Println("migrating dirwatches...")

	query = `SELECT s."systemId", s."systemRef", t."talkgroupId", t."talkgroupRef" FROM "systems" AS s LEFT JOIN "talkgroups" AS t`
	if rows, err = db.Sql.Query(query); err != nil {
		return formatError(err, query)
	}

	for rows.Next() {
		if err = rows.Scan(&systemId, &systemRef, &talkgroupId, &talkgroupRef); err != nil {
			continue
		}

		if systemId.Valid && systemRef.Valid && talkgroupId.Valid && talkgroupRef.Valid {
			if systems[systemRef.Int32] == 0 {
				systems[systemRef.Int32] = systemId.Int32
				talkgroups[systemRef.Int32] = map[int32]int32{}
			}

			talkgroups[systemRef.Int32][talkgroupRef.Int32] = talkgroupId.Int32
		}
	}

	rows.Close()

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "delay", "deleteAfter", "directory", "disabled", "extension", "frequency", "mask", "order", "systemId", "talkgroupId", "type" FROM "rdioScannerDirwatches"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		dirwatch := NewDirwatch()

		if err = rows.Scan(&dirwatchId, &delay, &deleteAfter, &directory, &disabled, &extension, &frequency, &mask, &order, &systemRef, &talkgroupRef, &kind); err != nil {
			continue
		}

		if dirwatchId.Valid {
			dirwatch.Id = uint64(dirwatchId.Int64)
		} else {
			continue
		}

		if delay.Valid {
			dirwatch.Delay = uint(delay.Int32)
		}

		if deleteAfter.Valid {
			dirwatch.DeleteAfter = deleteAfter.Bool
		}

		if directory.Valid && len(directory.String) > 0 {
			dirwatch.Directory = escapeQuotes(directory.String)
		} else {
			continue
		}

		if disabled.Valid {
			dirwatch.Disabled = disabled.Bool
		}

		if extension.Valid {
			dirwatch.Extension = escapeQuotes(extension.String)
		}

		if frequency.Valid {
			dirwatch.Frequency = uint(frequency.Int32)
		}

		if mask.Valid && len(mask.String) > 0 {
			dirwatch.Mask = escapeQuotes(mask.String)
		}

		if kind.Valid && len(kind.String) > 0 {
			dirwatch.Kind = kind.String
		}

		if order.Valid {
			dirwatch.Order = uint(order.Int32)
		}

		if systemRef.Valid && systems[systemRef.Int32] > 0 {
			refSystem = systems[systemRef.Int32]
		} else {
			refSystem = nil
		}

		if talkgroupId.Valid {
			refTalkgroup = talkgroups[systemRef.Int32][talkgroupRef.Int32]
		} else {
			refTalkgroup = nil
		}

		query = fmt.Sprintf(`INSERT INTO "dirwatches" ("dirwatchId", "delay", "deleteAfter", "directory", "disabled", "extension", "frequency", "mask", "order", "systemId", "talkgroupId", "type") VALUES (%d, %d, %t, '%s', %t, '%s', %d, '%s', %d, %d, %d, '%s')`, dirwatch.Id, dirwatch.Delay, dirwatch.DeleteAfter, dirwatch.Directory, dirwatch.Disabled, dirwatch.Extension, dirwatch.Frequency, dirwatch.Mask, dirwatch.Order, refSystem, refTalkgroup, dirwatch.Kind)
		if _, err = tx.Exec(query); err != nil {
			log.Println(formatError(err, query))
		}
	}

	rows.Close()

	query = `DROP TABLE "RdioScannerDirWatches"`
	if _, err = tx.Exec(`DROP TABLE "RdioScannerDirWatches"`); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateDownstreams(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		apikey       sql.NullString
		disabled     sql.NullBool
		downstreamId sql.NullInt64
		order        sql.NullInt32
		systems      sql.NullString
		url          sql.NullString
	)

	formatError := errorFormatter("migration", "migrateDownstreams")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerDownstreams"`); err != nil {
		return nil
	}

	log.Println("migrating downstreams...")

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "apiKey", "disabled", "order", "systems", "url" FROM "rdioScannerDownstreams"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		downstream := NewDownstream(nil)

		if err = rows.Scan(&downstreamId, &apikey, &disabled, &order, &systems, &url); err != nil {
			continue
		}

		if downstreamId.Valid {
			downstream.Id = uint64(downstreamId.Int64)
		} else {
			continue
		}

		if apikey.Valid && len(apikey.String) > 0 {
			downstream.Apikey = escapeQuotes(apikey.String)
		} else {
			continue
		}

		if disabled.Valid {
			downstream.Disabled = disabled.Bool
		}

		if order.Valid {
			downstream.Order = uint(order.Int32)
		}

		if systems.Valid && len(systems.String) > 0 {
			downstream.Systems = systems.String
		} else {
			continue
		}

		if url.Valid && len(url.String) > 0 {
			downstream.Url = escapeQuotes(url.String)
		} else {
			continue
		}

		query = fmt.Sprintf(`INSERT INTO "downstreams" ("downstreamId", "apikey", "disabled", "order", "systems", "url") VALUES (%d, '%s', %t, %d, '%s', '%s')`, downstream.Id, downstream.Apikey, downstream.Disabled, downstream.Order, downstream.Systems, downstream.Url)
		if _, err = tx.Exec(query); err != nil {
			log.Println(formatError(err, query))
		}
	}

	rows.Close()

	query = `DROP TABLE "rdioScannerDownstreams"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateGroups(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		groups  = []*Group{}
		groupId sql.NullInt32
		label   sql.NullString
	)

	formatError := errorFormatter("migration", "migrateGroups")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerGroups"`); err != nil {
		return nil
	}

	log.Println("migrating groups...")

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "label" FROM "rdioScannerGroups"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		group := NewGroup()

		if err = rows.Scan(&groupId, &label); err != nil {
			continue
		}

		if groupId.Valid {
			group.Id = uint64(groupId.Int32)
		} else {
			continue
		}

		if label.Valid {
			group.Label = escapeQuotes(label.String)
		}

		groups = append(groups, group)
	}

	rows.Close()

	sort.Slice(groups, func(i int, j int) bool {
		return groups[i].Label < groups[j].Label
	})

	for i, group := range groups {
		group.Order = uint(i + 1)

		query = fmt.Sprintf(`INSERT INTO "groups" ("groupId", "label", "order") VALUES (%d, '%s', %d)`, group.Id, group.Label, group.Order)
		if _, err = tx.Exec(query); err != nil {
			log.Println(formatError(err, query))
		}
	}

	query = `DROP TABLE "rdioScannerGroups"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateLogs(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		timestamp int64

		dateTime sql.NullTime
		level    sql.NullString
		logId    sql.NullInt32
		message  sql.NullString
	)

	formatError := errorFormatter("migration", "migrateLogs")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerLogs"`); err != nil {
		return nil
	}

	log.Println("migrating logs...")

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "dateTime", "level", "message" FROM "rdioScannerLogs"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		l := NewLog()

		if err = rows.Scan(&logId, &dateTime, &level, &message); err != nil {
			continue
		}

		if logId.Valid {
			l.Id = uint(logId.Int32)
		} else {
			continue
		}

		if dateTime.Valid {
			timestamp = dateTime.Time.UnixMilli()
		} else {
			continue
		}

		if level.Valid && len(level.String) > 0 {
			l.Level = level.String
		} else {
			continue
		}

		if message.Valid && len(message.String) > 0 {
			l.Message = escapeQuotes(message.String)
		} else {
			continue
		}

		query = fmt.Sprintf(`INSERT INTO "logs" ("logId", "level", "message", "timestamp") VALUES (%d, '%s', '%s', %d)`, l.Id, l.Level, l.Message, timestamp)
		if _, err = tx.Exec(query); err != nil {
			log.Println(formatError(err, query))
		}
	}

	rows.Close()

	query = `DROP TABLE "rdioScannerLogs"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateMeta(db *Database) error {
	formatError := errorFormatter("migration", "migrateMeta")

	if _, err := db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerMeta"`); err != nil {
		return nil
	}

	log.Println("migrating meta...")

	query := `DROP TABLE "rdioScannerMeta"`
	if _, err := db.Sql.Exec(query); err != nil {
		return formatError(err, query)
	}

	return nil
}

func migrateOptions(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		key   sql.NullString
		value sql.NullString
	)

	formatError := errorFormatter("migration", "migrateOptions")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerConfigs"`); err != nil {
		return nil
	}

	log.Println("migrating options...")

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "key", "val" FROM "rdioScannerConfigs"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		if err = rows.Scan(&key, &value); err != nil {
			continue
		}

		if !key.Valid || !value.Valid {
			continue
		}

		if key.String == "options" {
			var m map[string]any

			if err = json.Unmarshal([]byte(value.String), &m); err == nil {
				switch v := m["audioConversion"].(type) {
				case bool:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "audioConversion", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["autoPopulate"].(type) {
				case bool:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "autoPopulate", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["branding"].(type) {
				case string:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "branding", escapeQuotes(string(b)))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["dimmerDelay"].(type) {
				case float64:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "dimmerDelay", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["disableDuplicateDetection"].(type) {
				case bool:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "disableDuplicateDetection", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["duplicateDetectionTimeFrame"].(type) {
				case float64:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "duplicateDetectionTimeFrame", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["email"].(type) {
				case string:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "email", escapeQuotes(string(b)))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["keypadBeeps"].(type) {
				case string:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "keypadBeeps", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["maxClients"].(type) {
				case float64:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "maxClients", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["playbackGoesLive"].(type) {
				case bool:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "playbackGoesLive", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["pruneDays"].(type) {
				case float64:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "pruneDays", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["showListenersCount"].(type) {
				case bool:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "showListenersCount", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["sortTalkgroups"].(type) {
				case bool:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "sortTalkgroups", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
				switch v := m["time12hFormat"].(type) {
				case bool:
					if b, err := json.Marshal(v); err == nil {
						query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, "time12hFormat", string(b))
						if _, err = tx.Exec(query); err != nil {
							log.Println(formatError(err, query))
						}
					}
				}
			}

		} else {
			query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, escapeQuotes(key.String), escapeQuotes(value.String))
			if _, err = tx.Exec(query); err != nil {
				log.Println(formatError(err, query))
			}
		}
	}

	rows.Close()

	query = `DROP TABLE "rdioScannerConfigs"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateSystems(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		autoPopulate sql.NullBool
		blacklists   sql.NullString
		label        sql.NullString
		led          sql.NullString
		order        sql.NullInt32
		systemId     sql.NullInt64
		systemRef    sql.NullInt32
	)

	formatError := errorFormatter("migration", "migrateSystems")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerSystems"`); err != nil {
		return nil
	}

	log.Println("migrating systems...")

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "autoPopulate", "blacklists", "id", "label", "led", "order" FROM "rdioScannerSystems"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		system := NewSystem()

		if err = rows.Scan(&systemId, &autoPopulate, &blacklists, &systemRef, &label, &led, &order); err != nil {
			continue
		}

		if systemId.Valid {
			system.Id = uint64(systemId.Int64)
		} else {
			continue
		}

		if autoPopulate.Valid {
			system.AutoPopulate = autoPopulate.Bool
		}

		if blacklists.Valid {
			system.Blacklists = Blacklists(strings.ReplaceAll(strings.ReplaceAll(blacklists.String, "[", ""), "]", ""))
		}

		if label.Valid {
			system.Label = escapeQuotes(label.String)
		}

		if led.Valid {
			system.Led = led.String
		}

		if order.Valid {
			system.Order = uint(order.Int32)
		}

		if systemRef.Valid {
			system.SystemRef = uint(systemRef.Int32)
		}

		query = fmt.Sprintf(`INSERT INTO "systems" ("systemId", "autoPopulate", "blacklists", "label", "led", "order", "systemRef") VALUES (%d, %t, '%s', '%s', '%s', %d, %d)`, system.Id, system.AutoPopulate, system.Blacklists, system.Label, system.Led, system.Order, system.SystemRef)
		if _, err = tx.Exec(query); err != nil {
			log.Println(formatError(err, query))
		}
	}

	rows.Close()

	query = `DROP TABLE "rdioScannerSystems"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateTags(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		label sql.NullString
		tags  = []*Tag{}
		tagId sql.NullInt32
	)

	formatError := errorFormatter("migration", "migrateTags")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerTags"`); err != nil {
		return nil
	}

	log.Println("migrating tags...")

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "label" FROM "rdioScannerTags"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		tag := NewTag()

		if err = rows.Scan(&tagId, &label); err != nil {
			continue
		}

		if tagId.Valid {
			tag.Id = uint64(tagId.Int32)
		} else {
			continue
		}

		if label.Valid {
			tag.Label = escapeQuotes(label.String)
		}

		tags = append(tags, tag)
	}

	rows.Close()

	sort.Slice(tags, func(i int, j int) bool {
		return tags[i].Label < tags[j].Label
	})

	for i, tag := range tags {
		tag.Order = uint(i + 1)

		query = fmt.Sprintf(`INSERT INTO "tags" ("tagId", "label", "order") VALUES (%d, '%s', %d)`, tag.Id, tag.Label, tag.Order)
		if _, err = tx.Exec(query); err != nil {
			log.Println(formatError(err, query))
		}
	}

	query = `DROP TABLE "rdioScannerTags"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateTalkgroups(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		systems = map[int64]int64{}

		frequency    sql.NullInt32
		groupId      sql.NullInt64
		label        sql.NullString
		led          sql.NullString
		name         sql.NullString
		order        sql.NullInt32
		systemId     sql.NullInt64
		tagId        sql.NullInt64
		talkgroupId  sql.NullInt64
		talkgroupRef sql.NullInt32
	)

	formatError := errorFormatter("migration", "migrateTalkgroups")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerTalkgroups"`); err != nil {
		return nil
	}

	log.Println("migrating talkgroups...")

	query = `SELECT "systemId", "systemRef" FROM "systems"`
	if rows, err = db.Sql.Query(query); err != nil {
		return formatError(err, query)
	}

	for rows.Next() {
		if err = rows.Scan(&systemId, &talkgroupRef); err != nil {
			continue
		}

		if systemId.Valid && talkgroupRef.Valid {
			systems[int64(talkgroupRef.Int32)] = systemId.Int64
		}
	}

	rows.Close()

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "frequency", "groupId", "id", "label", "led", "name", "order", "systemId", "tagId" FROM "rdioScannerTalkgroups"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		talkgroup := NewTalkgroup()

		if err = rows.Scan(&talkgroupId, &frequency, &groupId, &talkgroupRef, &label, &led, &name, &order, &systemId, &tagId); err != nil {
			continue
		}

		if talkgroupId.Valid {
			talkgroup.Id = uint64(talkgroupId.Int64)
		} else {
			continue
		}

		if frequency.Valid {
			talkgroup.Frequency = uint(frequency.Int32)
		}

		if groupId.Valid {
			talkgroup.GroupIds = []uint64{uint64(groupId.Int64)}
		}

		if label.Valid {
			talkgroup.Label = escapeQuotes(label.String)
		}

		if led.Valid {
			talkgroup.Led = led.String
		}

		if name.Valid {
			talkgroup.Name = escapeQuotes(name.String)
		}

		if order.Valid {
			talkgroup.Order = uint(order.Int32)
		}

		if !systemId.Valid || systems[systemId.Int64] == 0 {
			continue
		}

		if talkgroupRef.Valid {
			talkgroup.TalkgroupRef = uint(talkgroupRef.Int32)
		}

		if tagId.Valid {
			talkgroup.TagId = uint64(tagId.Int64)
		}

		query = fmt.Sprintf(`INSERT INTO "talkgroups" ("talkgroupId", "frequency", "label", "led", "name", "order", "systemId", "tagId", "talkgroupRef") VALUES (%d, %d, '%s', '%s', '%s', %d, %d, %d, %d)`, talkgroup.Id, talkgroup.Frequency, talkgroup.Label, talkgroup.Led, talkgroup.Name, talkgroup.Order, systems[systemId.Int64], talkgroup.TagId, talkgroup.TalkgroupRef)
		if _, err = tx.Exec(query); err == nil {
			query = fmt.Sprintf(`INSERT INTO "talkgroupGroups" ("groupId", "talkgroupId") VALUES (%d, %d)`, talkgroup.GroupIds[0], talkgroup.Id)
			if _, err = tx.Exec(query); err != nil {
				log.Println(formatError(err, query))
			}

		} else {
			log.Println(formatError(err, query))
		}
	}

	rows.Close()

	query = `DROP TABLE "rdioScannerTalkgroups"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}

func migrateUnits(db *Database) error {
	var (
		err   error
		query string
		rows  *sql.Rows
		tx    *sql.Tx

		systems = map[int32]int32{}

		label    sql.NullString
		order    sql.NullInt32
		systemId sql.NullInt32
		unitId   sql.NullInt64
		unitRef  sql.NullInt32
	)

	formatError := errorFormatter("migration", "migrateUnits")

	if _, err = db.Sql.Exec(`SELECT COUNT(*) FROM "rdioScannerUnits"`); err != nil {
		return nil
	}

	log.Println("migrating units...")

	query = `SELECT "systemId", "systemRef" FROM "systems"`
	if rows, err = db.Sql.Query(query); err != nil {
		return formatError(err, query)
	}

	for rows.Next() {
		if err = rows.Scan(&systemId, &unitRef); err != nil {
			continue
		}

		if systemId.Valid && unitRef.Valid {
			systems[unitRef.Int32] = systemId.Int32
		}
	}

	rows.Close()

	if tx, err = db.Sql.Begin(); err != nil {
		return formatError(err, "")
	}

	query = `SELECT "_id", "id", "label", "order", "systemId" FROM "rdioScannerUnits"`
	if rows, err = tx.Query(query); err != nil {
		tx.Rollback()
		return formatError(err, query)
	}

	for rows.Next() {
		unit := NewUnit()

		if err = rows.Scan(&unitId, &unitRef, &label, &order, &systemId); err != nil {
			continue
		}

		if !unitId.Valid {
			continue
		}

		if !systemId.Valid || systems[systemId.Int32] == 0 {
			continue
		}

		if label.Valid {
			unit.Label = escapeQuotes(label.String)
		}

		if order.Valid {
			unit.Order = uint(order.Int32)
		}

		if unitRef.Valid {
			unit.UnitRef = uint(unitRef.Int32)
		}

		query = fmt.Sprintf(`INSERT INTO "units" ("unitId", "label", "order", "systemId", "unitRef") VALUES (%d, '%s', %d, %d, %d)`, unitId.Int64, unit.Label, unit.Order, systems[systemId.Int32], unit.Id)
		if _, err = tx.Exec(query); err != nil {
			log.Println(formatError(err, query))
		}
	}

	rows.Close()

	query = `DROP TABLE "rdioScannerUnits"`
	if _, err = tx.Exec(query); err != nil {
		log.Println(formatError(err, query))
	}

	if err = tx.Commit(); err != nil {
		tx.Rollback()
		return formatError(err, "")
	}

	return nil
}
