package no.svorka.mcms.core

import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put

class AuthRepository(
    private val client: ApiClient,
    private val session: SessionStore,
    private val config: ApiConfiguration,
) {
    suspend fun login(email: String, password: String) {
        val body = buildJsonObject { put("email", email); put("password", password) }
        // Login body is unwrapped (matches the official 6.2 dev guide).
        client.send("POST", McmsEndpoint.Authenticate, body, wrapInData = false)
        session.markAuthenticated()
        config.databaseId?.let { selectDatabase(it) }
    }

    suspend fun selectDatabase(id: String) {
        // `{ "data": "<db id>" }` — a bare string.
        client.send("PUT", McmsEndpoint.DatabaseSelection, JsonPrimitive(id), wrapInData = true)
    }

    suspend fun logout() {
        runCatching { client.action("GET", McmsEndpoint.Logout) }
        session.clear()
    }

    suspend fun version(): VersionInfo = VersionInfo(client.getElement(McmsEndpoint.Version))
}

class OnuRepository(private val client: ApiClient) {
    suspend fun states(query: ApiQuery = ApiQuery.EMPTY): List<OnuStateDoc> =
        client.getList(McmsEndpoint.OnuStates, query).map { OnuStateDoc(it) }

    suspend fun allStates(): List<OnuStateDoc> =
        client.getAll(McmsEndpoint.OnuStates) { it["_id"].string }.map { OnuStateDoc(it) }

    suspend fun state(id: String): OnuStateDoc = OnuStateDoc(client.getElement(McmsEndpoint.OnuState(id)))
    suspend fun config(id: String): OnuConfigDoc = OnuConfigDoc(client.getElement(McmsEndpoint.OnuConfig(id)))

    /** id → operator name via projection (only `ONU.Name` comes back). */
    suspend fun nameMap(): Map<String, String> {
        val q = ApiQuery(projection = mapOf("ONU.Name" to 1))
        return client.getAll(McmsEndpoint.OnuConfigs, baseQuery = q) { it["_id"].string }
            .map { OnuConfigDoc(it) }
            .mapNotNull { c -> c.name?.takeIf { it.isNotEmpty() }?.let { c.id to it } }
            .toMap()
    }

    suspend fun stats(id: String, lastHours: Int = 24): List<StatSample> =
        client.getList(McmsEndpoint.OnuStats(id), ApiQuery.window(lastHours)).map { StatSample(it) }

    suspend fun logs(id: String, lastHours: Int = 24): List<LogEntry> =
        client.getList(McmsEndpoint.OnuLogs(id), ApiQuery.window(lastHours)).map { LogEntry(it) }

    suspend fun alarmHistories(query: ApiQuery = ApiQuery.EMPTY): List<AlarmHistoryDoc> =
        client.getList(McmsEndpoint.OnuAlarmHistories, query).map { AlarmHistoryDoc(it) }

    /** CPE DHCP state from the `/cpe/onu/<id>/` helper — a bare `[code, cpe…]` array. */
    suspend fun cpes(id: String): List<CpeState> =
        client.getRaw(McmsEndpoint.OnuCpe(id)).array?.filter { it.isObject }?.map { CpeState(it) } ?: emptyList()

    suspend fun reset(id: String) { client.action("PUT", McmsEndpoint.OnuReset(id)) }

    suspend fun firmwareFiles(): List<FirmwareFile> =
        client.getList(McmsEndpoint.OnuFirmwareFiles).map { FirmwareFile(it) }

    /** Procedure 7: stage firmware in the inactive bank. Returns the slot. Service-affecting. */
    suspend fun upgradeFirmware(id: String, file: FirmwareFile): Int {
        val filename = file.filename ?: throw ApiError.Encoding("Firmware file is missing a filename.")
        val version = file.version ?: throw ApiError.Encoding("Firmware file is missing a version.")
        val current = config(id)
        val result = FirmwareUpgrade.apply(current.raw, filename, version)
            ?: throw ApiError.Encoding("Unexpected ONU-CFG shape — firmware write aborted.")
        client.send("PUT", McmsEndpoint.OnuConfig(id), result.document, wrapInData = true)
        return result.slot
    }
}

class OltRepository(private val client: ApiClient) {
    suspend fun states(query: ApiQuery = ApiQuery.EMPTY): List<OltStateDoc> =
        client.getList(McmsEndpoint.OltStates, query).map { OltStateDoc(it) }

    suspend fun allStates(): List<OltStateDoc> =
        client.getAll(McmsEndpoint.OltStates) { it["_id"].string }.map { OltStateDoc(it) }

    suspend fun state(id: String): OltStateDoc = OltStateDoc(client.getElement(McmsEndpoint.OltState(id)))

    suspend fun allConfigs(): List<OltConfigDoc> =
        client.getAll(McmsEndpoint.OltConfigs) { it["_id"].string }.map { OltConfigDoc(it) }

    suspend fun nameMap(): Map<String, String> {
        val q = ApiQuery(projection = mapOf("OLT.Name" to 1))
        return client.getAll(McmsEndpoint.OltConfigs, baseQuery = q) { it["_id"].string }
            .map { OltConfigDoc(it) }
            .mapNotNull { c -> c.name?.takeIf { it.isNotEmpty() && it != c.id }?.let { c.id to it } }
            .toMap()
    }

    suspend fun reset(id: String) { client.action("PUT", McmsEndpoint.OltReset(id)) }

    suspend fun registrationMap(): Map<String, OnuLifecycleState> =
        OltRepository.registrationMap(allStates())

    companion object {
        /** Invert OLT-STATE buckets → onuId: state; prefer non-Registered on duplicate (§3.11). */
        fun registrationMap(olts: List<OltStateDoc>): Map<String, OnuLifecycleState> {
            val map = mutableMapOf<String, OnuLifecycleState>()
            for (olt in olts) {
                for ((bucket, ids) in olt.onuStatesByBucket) {
                    val state = OnuLifecycleState.fromBucket(bucket)
                    for (id in ids) {
                        val existing = map[id]
                        when {
                            existing == null -> map[id] = state
                            existing == OnuLifecycleState.REGISTERED && state != OnuLifecycleState.REGISTERED -> map[id] = state
                        }
                    }
                }
            }
            return map
        }
    }
}

class ControllerRepository(private val client: ApiClient) {
    suspend fun allConfigs(): List<ControllerConfigDoc> =
        client.getAll(McmsEndpoint.ControllerConfigs) { it["_id"].string }.map { ControllerConfigDoc(it) }
}
