package de.tsschulz.timeclock.data.api

import de.tsschulz.timeclock.config.AppConfig
import de.tsschulz.timeclock.data.auth.TokenStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.concurrent.TimeUnit

private val JsonMedia = "application/json; charset=utf-8".toMediaType()

class TimeClockApiClient(
    private val tokenStore: TokenStore,
    private val json: Json = Json {
        ignoreUnknownKeys = true
        isLenient = true
        coerceInputValues = true
    },
    private val client: OkHttpClient = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build(),
    private val baseUrl: String = AppConfig.apiBaseUrl,
) {

    private fun endpoint(path: String): String {
        val base = baseUrl.trimEnd('/')
        val p = path.trimStart('/')
        return "$base/$p"
    }

    suspend fun postLogin(body: LoginRequest): LoginResponse {
        val raw = execute(
            Request.Builder()
                .url(endpoint("auth/login"))
                .post(json.encodeToString(LoginRequest.serializer(), body).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(LoginResponse.serializer(), raw)
    }

    suspend fun getMe(): MeResponse {
        val t = tokenStore.getToken() ?: throw ApiException("Nicht angemeldet", 401)
        val raw = execute(
            Request.Builder()
                .url(endpoint("auth/me"))
                .header("Authorization", "Bearer $t")
                .get()
                .build(),
        )
        return decode(MeResponse.serializer(), raw)
    }

    suspend fun postLogout(): LogoutResponse {
        val t = tokenStore.getToken() ?: throw ApiException("Nicht angemeldet", 401)
        val raw = execute(
            Request.Builder()
                .url(endpoint("auth/logout"))
                .header("Authorization", "Bearer $t")
                .post("{}".toRequestBody(JsonMedia))
                .build(),
        )
        return decode(LogoutResponse.serializer(), raw.ifBlank { "{}" })
    }

    fun googleLoginUrl(): String = endpoint("auth/google?platform=android")

    suspend fun createGoogleLinkUrl(): OAuthLinkUrlResponse {
        val raw = execute(
            authorized("auth/google/link-url")
                .post(json.encodeToString(OAuthLinkUrlRequest.serializer(), OAuthLinkUrlRequest()).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(OAuthLinkUrlResponse.serializer(), raw)
    }

    suspend fun linkExistingOAuthAccount(pendingToken: String, email: String, password: String): LoginResponse {
        val raw = execute(
            Request.Builder()
                .url(endpoint("auth/oauth/link-existing"))
                .post(
                    json.encodeToString(
                        OAuthLinkExistingRequest.serializer(),
                        OAuthLinkExistingRequest(pendingToken, email, password),
                    ).toRequestBody(JsonMedia),
                )
                .build(),
        )
        return decode(LoginResponse.serializer(), raw)
    }

    suspend fun getCurrentState(): CurrentStateResponse {
        val raw = execute(authorized("time-entries/current-state").get().build())
        return decode(CurrentStateResponse.serializer(), raw)
    }

    suspend fun getRunningEntry(): RunningEntryDto {
        val raw = execute(authorized("time-entries/running").get().build())
        return decode(RunningEntryDto.serializer(), raw.ifBlank { "{}" })
    }

    suspend fun getTimeStats(): TimeStatsDto {
        val raw = execute(authorized("time-entries/stats/summary").get().build())
        return decode(TimeStatsDto.serializer(), raw.ifBlank { "{}" })
    }

    suspend fun postClock(action: String): ClockResponse {
        val raw = execute(
            authorized("time-entries/clock")
                .post(json.encodeToString(ClockRequest.serializer(), ClockRequest(action)).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(ClockResponse.serializer(), raw)
    }

    suspend fun getWeekOverview(weekOffset: Int): WeekOverviewResponse {
        val raw = execute(authorized("week-overview?weekOffset=$weekOffset").get().build())
        return decode(WeekOverviewResponse.serializer(), raw)
    }

    suspend fun getTimeEntries(): List<TimeEntryDto> =
        decode(ListSerializer(TimeEntryDto.serializer()), execute(authorized("time-entries").get().build()))

    suspend fun deleteTimeEntry(id: String) {
        execute(authorized("time-entries/$id").delete().build())
    }

    suspend fun getVacations(): List<VacationDto> =
        decode(ListSerializer(VacationDto.serializer()), execute(authorized("vacation").get().build()))

    suspend fun createVacation(request: VacationCreateRequest) {
        execute(
            authorized("vacation")
                .post(json.encodeToString(VacationCreateRequest.serializer(), request).toRequestBody(JsonMedia))
                .build(),
        )
    }

    suspend fun deleteVacation(id: String) {
        execute(authorized("vacation/$id").delete().build())
    }

    suspend fun getSickEntries(): List<SickEntryDto> =
        decode(ListSerializer(SickEntryDto.serializer()), execute(authorized("sick").get().build()))

    suspend fun getSickTypes(): List<SickTypeDto> =
        decode(ListSerializer(SickTypeDto.serializer()), execute(authorized("sick/types").get().build()))

    suspend fun createSick(request: SickCreateRequest) {
        execute(
            authorized("sick")
                .post(json.encodeToString(SickCreateRequest.serializer(), request).toRequestBody(JsonMedia))
                .build(),
        )
    }

    suspend fun deleteSick(id: String) {
        execute(authorized("sick/$id").delete().build())
    }

    suspend fun getTimefixes(): List<TimefixDto> =
        decode(ListSerializer(TimefixDto.serializer()), execute(authorized("timefix").get().build()))

    suspend fun getWorklogEntries(date: String): List<WorklogEntryDto> =
        decode(ListSerializer(WorklogEntryDto.serializer()), execute(authorized("timefix/worklog-entries?date=$date").get().build()))

    suspend fun createTimefix(request: TimefixCreateRequest) {
        execute(
            authorized("timefix")
                .post(json.encodeToString(TimefixCreateRequest.serializer(), request).toRequestBody(JsonMedia))
                .build(),
        )
    }

    suspend fun deleteTimefix(id: String) {
        execute(authorized("timefix/$id").delete().build())
    }

    suspend fun getWorkdays(year: Int): WorkdaysDto =
        decode(WorkdaysDto.serializer(), execute(authorized("workdays?year=$year").get().build()))

    suspend fun getCalendar(year: Int, month: Int): CalendarDto =
        decode(CalendarDto.serializer(), execute(authorized("calendar?year=$year&month=$month").get().build()))

    suspend fun getProfile(): ProfileDto =
        decode(ProfileDto.serializer(), execute(authorized("profile").get().build()))

    suspend fun getStates(): List<StateDto> =
        decode(ListSerializer(StateDto.serializer()), execute(authorized("profile/states").get().build()))

    suspend fun updateProfile(request: ProfileUpdateRequest): MessageResponse {
        val raw = execute(
            authorized("profile")
                .put(json.encodeToString(ProfileUpdateRequest.serializer(), request).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(MessageResponse.serializer(), raw.ifBlank { "{}" })
    }

    suspend fun changePassword(request: PasswordChangeRequest): MessageResponse {
        val raw = execute(
            authorized("password")
                .put(json.encodeToString(PasswordChangeRequest.serializer(), request).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(MessageResponse.serializer(), raw.ifBlank { "{}" })
    }

    suspend fun getTimewishes(): List<TimewishDto> =
        decode(ListSerializer(TimewishDto.serializer()), execute(authorized("timewish").get().build()))

    suspend fun createTimewish(request: TimewishCreateRequest): MessageResponse {
        val raw = execute(
            authorized("timewish")
                .post(json.encodeToString(TimewishCreateRequest.serializer(), request).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(MessageResponse.serializer(), raw.ifBlank { "{}" })
    }

    suspend fun deleteTimewish(id: String) {
        execute(authorized("timewish/$id").delete().build())
    }

    suspend fun getInvites(): List<InvitationDto> =
        decode(ListSerializer(InvitationDto.serializer()), execute(authorized("invite").get().build()))

    suspend fun sendInvite(request: InviteRequest): InvitationDto {
        val raw = execute(
            authorized("invite")
                .post(json.encodeToString(InviteRequest.serializer(), request).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(InvitationDto.serializer(), raw)
    }

    suspend fun getWatchers(): List<WatcherDto> =
        decode(ListSerializer(WatcherDto.serializer()), execute(authorized("watcher").get().build()))

    suspend fun addWatcher(request: WatcherRequest): WatcherDto {
        val raw = execute(
            authorized("watcher")
                .post(json.encodeToString(WatcherRequest.serializer(), request).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(WatcherDto.serializer(), raw)
    }

    suspend fun deleteWatcher(id: String) {
        execute(authorized("watcher/$id").delete().build())
    }

    suspend fun getHolidayStates(): List<HolidayStateDto> =
        decode(ListSerializer(HolidayStateDto.serializer()), execute(authorized("holidays/states").get().build()))

    suspend fun getHolidays(): HolidaysResponse =
        decode(HolidaysResponse.serializer(), execute(authorized("holidays").get().build()))

    suspend fun createHoliday(request: HolidayCreateRequest): HolidayDto {
        val raw = execute(
            authorized("holidays")
                .post(json.encodeToString(HolidayCreateRequest.serializer(), request).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(HolidayDto.serializer(), raw)
    }

    suspend fun deleteHoliday(id: String) {
        execute(authorized("holidays/$id").delete().build())
    }

    suspend fun getRoleUsers(): List<RoleUserDto> =
        decode(ListSerializer(RoleUserDto.serializer()), execute(authorized("roles/users").get().build()))

    suspend fun updateUserRole(id: String, role: Int): RoleUserDto {
        val raw = execute(
            authorized("roles/users/$id")
                .put(json.encodeToString(RoleUpdateRequest.serializer(), RoleUpdateRequest(role)).toRequestBody(JsonMedia))
                .build(),
        )
        return decode(RoleUserDto.serializer(), raw)
    }

    private fun authorized(path: String): Request.Builder {
        val t = tokenStore.getToken() ?: throw ApiException("Nicht angemeldet", 401)
        return Request.Builder()
            .url(endpoint(path))
            .header("Authorization", "Bearer $t")
    }

    private suspend fun execute(request: Request): String = withContext(Dispatchers.IO) {
        client.newCall(request).execute().use { response ->
            val raw = response.body?.string().orEmpty()
            if (response.code == 401) {
                tokenStore.clearToken()
            }
            if (!response.isSuccessful) {
                val err = runCatching { json.decodeFromString(ErrorBody.serializer(), raw) }.getOrNull()
                val msg = err?.error ?: err?.message ?: "HTTP ${response.code}"
                throw ApiException(msg, response.code)
            }
            raw
        }
    }

    private fun <T> decode(deserializer: KSerializer<T>, raw: String): T =
        if (raw.isBlank()) json.decodeFromString(deserializer, "{}")
        else json.decodeFromString(deserializer, raw)
}
