/*
 * Copyright (c) 2026 Meshtastic LLC
 *
 * 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 <https://www.gnu.org/licenses/>.
 */
package org.meshtastic.core.database

import androidx.room3.AutoMigration
import androidx.room3.ColumnTypeConverters
import androidx.room3.Database
import androidx.room3.DeleteColumn
import androidx.room3.DeleteTable
import androidx.room3.RoomDatabase
import androidx.room3.migration.AutoMigrationSpec
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.database.dao.DeviceHardwareDao
import org.meshtastic.core.database.dao.DeviceLinkDao
import org.meshtastic.core.database.dao.DiscoveryDao
import org.meshtastic.core.database.dao.FirmwareReleaseDao
import org.meshtastic.core.database.dao.MeshLogDao
import org.meshtastic.core.database.dao.NodeInfoDao
import org.meshtastic.core.database.dao.PacketDao
import org.meshtastic.core.database.dao.QuickChatActionDao
import org.meshtastic.core.database.dao.TracerouteNodePositionDao
import org.meshtastic.core.database.entity.ContactSettings
import org.meshtastic.core.database.entity.DeviceHardwareEntity
import org.meshtastic.core.database.entity.DeviceLinkEntity
import org.meshtastic.core.database.entity.DiscoveredNodeEntity
import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity
import org.meshtastic.core.database.entity.DiscoverySessionEntity
import org.meshtastic.core.database.entity.FirmwareReleaseEntity
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.MetadataEntity
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.entity.PacketFts
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.database.entity.ReactionEntity
import org.meshtastic.core.database.entity.TracerouteNodePositionEntity

@Database(
    entities =
    [
        MyNodeEntity::class,
        NodeEntity::class,
        Packet::class,
        PacketFts::class,
        ContactSettings::class,
        MeshLog::class,
        QuickChatAction::class,
        ReactionEntity::class,
        MetadataEntity::class,
        DeviceHardwareEntity::class,
        DeviceLinkEntity::class,
        FirmwareReleaseEntity::class,
        TracerouteNodePositionEntity::class,
        DiscoverySessionEntity::class,
        DiscoveryPresetResultEntity::class,
        DiscoveredNodeEntity::class,
    ],
    autoMigrations =
    [
        AutoMigration(from = 3, to = 4),
        AutoMigration(from = 4, to = 5),
        AutoMigration(from = 5, to = 6),
        AutoMigration(from = 6, to = 7),
        AutoMigration(from = 7, to = 8),
        AutoMigration(from = 8, to = 9),
        AutoMigration(from = 9, to = 10),
        AutoMigration(from = 10, to = 11),
        AutoMigration(from = 11, to = 12),
        AutoMigration(from = 12, to = 13, spec = AutoMigration12to13::class),
        AutoMigration(from = 13, to = 14),
        AutoMigration(from = 14, to = 15),
        AutoMigration(from = 15, to = 16),
        AutoMigration(from = 16, to = 17),
        AutoMigration(from = 17, to = 18),
        AutoMigration(from = 18, to = 19),
        AutoMigration(from = 19, to = 20),
        AutoMigration(from = 20, to = 21),
        AutoMigration(from = 21, to = 22),
        AutoMigration(from = 22, to = 23),
        AutoMigration(from = 23, to = 24),
        AutoMigration(from = 24, to = 25),
        AutoMigration(from = 25, to = 26),
        AutoMigration(from = 26, to = 27),
        AutoMigration(from = 27, to = 28),
        AutoMigration(from = 28, to = 29),
        AutoMigration(from = 29, to = 30, spec = AutoMigration29to30::class),
        AutoMigration(from = 30, to = 31),
        AutoMigration(from = 31, to = 32),
        AutoMigration(from = 32, to = 33),
        AutoMigration(from = 33, to = 34, spec = AutoMigration33to34::class),
        AutoMigration(from = 34, to = 35, spec = AutoMigration34to35::class),
        AutoMigration(from = 35, to = 36),
        AutoMigration(from = 36, to = 37),
        AutoMigration(from = 37, to = 38),
        AutoMigration(from = 38, to = 39),
        AutoMigration(from = 39, to = 40),
        AutoMigration(from = 40, to = 41),
        AutoMigration(from = 41, to = 42),
        AutoMigration(from = 42, to = 43, spec = AutoMigration42to43::class),
        AutoMigration(from = 43, to = 44),
    ],
    version = 44,
    exportSchema = true,
)
@androidx.room3.ConstructedBy(MeshtasticDatabaseConstructor::class)
@ColumnTypeConverters(Converters::class)
@androidx.room3.DaoReturnTypeConverters(androidx.room3.paging.PagingSourceDaoReturnTypeConverter::class)
abstract class MeshtasticDatabase : RoomDatabase() {
    abstract fun nodeInfoDao(): NodeInfoDao

    abstract fun packetDao(): PacketDao

    abstract fun meshLogDao(): MeshLogDao

    abstract fun quickChatActionDao(): QuickChatActionDao

    abstract fun deviceHardwareDao(): DeviceHardwareDao

    abstract fun deviceLinkDao(): DeviceLinkDao

    abstract fun firmwareReleaseDao(): FirmwareReleaseDao

    abstract fun tracerouteNodePositionDao(): TracerouteNodePositionDao

    abstract fun discoveryDao(): DiscoveryDao

    companion object {
        /**
         * Configures a [RoomDatabase.Builder] with standard settings for this project.
         *
         * @param multiConnection when `true` (default), opens a multi-reader connection pool (`maxNumOfReaders = 4`,
         *   `maxNumOfWriters = 1`) so reads can run concurrently. Pass `false` to explicitly force
         *   [setSingleConnectionPool], serializing all reads and writes through one connection.
         *
         * **Android production passes `false`.** Without the explicit `setSingleConnectionPool()` call, Room defaults
         * to a 4-reader pool for named databases regardless of whether `setMultipleConnectionPool` was called. Under
         * coroutine cancellation churn (e.g. DB switches via `flatMapLatest`), the reader-pool permit semaphore can
         * wedge: all reader connections report `Free` but `permits=0`, so every read acquisition times out
         * indefinitely. Forcing single-connection eliminates the separate reader permit pool entirely.
         *
         * **In-memory databases (tests) pass `false`.** Room already serves an in-memory database (`name == null`) from
         * a single connection regardless of the pool configuration, so this only makes the single-connection intent
         * explicit and serializes the query dispatcher; it is not load-bearing for read-after-write.
         * (`DeviceLinkRepositoryImplTest` is deterministic because it runs on the wall clock, not because of this flag;
         * see that test's comments.)
         *
         * **JVM/iOS production uses `true`** (the default). Revisit if desktop/iOS field logs show similar
         * pool-exhaustion patterns under cancellation churn.
         */
        @OptIn(ExperimentalCoroutinesApi::class)
        fun <T : RoomDatabase> RoomDatabase.Builder<T>.configureCommon(
            multiConnection: Boolean = true,
        ): RoomDatabase.Builder<T> = this.fallbackToDestructiveMigration(dropAllTables = false)
            .apply {
                if (multiConnection) {
                    setMultipleConnectionPool(maxNumOfReaders = 4, maxNumOfWriters = 1)
                } else {
                    setSingleConnectionPool()
                }
            }
            .setQueryCoroutineContext(
                // limitedParallelism(1) has the same throughput ceiling as the single-connection pool
                // (already serialized), so this only blocks the cancellation pileup — not real I/O concurrency.
                if (multiConnection) ioDispatcher else ioDispatcher.limitedParallelism(1),
            )
    }
}

@DeleteTable(tableName = "NodeInfo")
@DeleteTable(tableName = "MyNodeInfo")
class AutoMigration12to13 : AutoMigrationSpec

@DeleteColumn(tableName = "packet", columnName = "reply_id")
class AutoMigration29to30 : AutoMigrationSpec

@DeleteColumn(tableName = "packet", columnName = "retry_count")
@DeleteColumn(tableName = "reactions", columnName = "retry_count")
class AutoMigration33to34 : AutoMigrationSpec

@DeleteColumn(tableName = "packet", columnName = "retry_count")
@DeleteColumn(tableName = "reactions", columnName = "retry_count")
class AutoMigration34to35 : AutoMigrationSpec

/** Device links moved from the bundled `urls.json` to the resolved API; `original_url` is no longer stored. */
@DeleteColumn(tableName = "device_link", columnName = "original_url")
class AutoMigration42to43 : AutoMigrationSpec
