// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.repo

import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.api.LineV1
import xyz.apiote.bimba.czwek.api.LineV2
import xyz.apiote.bimba.czwek.api.LineV3
import xyz.apiote.bimba.czwek.api.PositionV1
import xyz.apiote.bimba.czwek.api.Server
import xyz.apiote.bimba.czwek.api.StopV1
import xyz.apiote.bimba.czwek.api.StopV2
import xyz.apiote.bimba.czwek.api.StopV3
import xyz.apiote.bimba.czwek.api.UnknownResourceException
import xyz.apiote.bimba.czwek.api.VehicleV1
import xyz.apiote.bimba.czwek.api.VehicleV2
import xyz.apiote.bimba.czwek.api.VehicleV3
import xyz.apiote.bimba.czwek.api.getTransitousDepartures
import xyz.apiote.bimba.czwek.api.getTransitousQueryables
import xyz.apiote.bimba.czwek.api.locateTransitousQueryables
import xyz.apiote.bimba.czwek.api.responses.DeparturesResponse
import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseDev
import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseV1
import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseV2
import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseV3
import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseV4
import xyz.apiote.bimba.czwek.api.responses.ErrorResponse
import xyz.apiote.bimba.czwek.api.responses.FeedsResponse
import xyz.apiote.bimba.czwek.api.responses.FeedsResponseDev
import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV1
import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV2
import xyz.apiote.bimba.czwek.api.responses.LineResponse
import xyz.apiote.bimba.czwek.api.responses.LineResponseDev
import xyz.apiote.bimba.czwek.api.responses.LineResponseV1
import xyz.apiote.bimba.czwek.api.responses.LineResponseV2
import xyz.apiote.bimba.czwek.api.responses.LineResponseV3
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponse
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseDev
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseV1
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseV2
import xyz.apiote.bimba.czwek.api.responses.LocatablesResponseV3
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponse
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseDev
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV1
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV2
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV3
import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV4
import xyz.apiote.bimba.czwek.units.Metre
import java.time.LocalDate

// todo [3.2] in Repository check if responses are BARE or HTML

class OnlineRepository : Repository {
	override suspend fun getFavourite(stopCode: String): Favourite? {
		TODO("Not yet implemented; waits for ampelmänchen")
	}

	override suspend fun getFavourites(feedIDs: Set<String>): List<Favourite> {
		TODO("Not yet implemented; waits for ampelmänchen")
	}

	override suspend fun saveFavourite(favourite: Favourite) {
		TODO("Not yet implemented; waits for ampelmänchen")
	}

	override suspend fun saveFavourites(favourites: Set<Favourite>) {
		TODO("Not yet implemented; waits for ampelmänchen")
	}

	override suspend fun getFeeds(
		context: Context,
		server: Server
	): Map<String, FeedInfo>? {
		val result =
			xyz.apiote.bimba.czwek.api.getFeeds(context, server)
		if (result.error != null) {
			if (result.stream != null) {
				val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) }
				throw TrafficResponseException(result.error.statusCode, response.message, result.error)
			} else {
				throw TrafficResponseException(result.error.statusCode, "", result.error)
			}
		} else {
			val rawResponse = result.stream!!.readBytes()
			val feeds = when (val response =
				withContext(Dispatchers.IO) { FeedsResponse.unmarshal(rawResponse.inputStream()) }) {
				is FeedsResponseDev -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }
				is FeedsResponseV2 -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }
				is FeedsResponseV1 -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }

				else -> null
			}
			val feedsWithTransitous = feeds?.toMutableMap()
			feedsWithTransitous?.put(
				"transitous", FeedInfo(
					"transitous", "Transitous",
					context.getString(R.string.transitous_description),
					context.getString(R.string.transitous_attribution),
					LocalDate.now(), "", QrLocation.NONE, "", null, null, false
				)
			)
			return feedsWithTransitous
		}
	}

	override suspend fun getDepartures(
		feedID: String,
		stop: String,
		date: LocalDate?,
		context: Context,
		limit: Int?
	): StopEvents? {
		return if (feedID == "transitous") {
			getTransitousDepartures(context, stop, date, limit)
		} else {
			val result = xyz.apiote.bimba.czwek.api.getDepartures(
				context,
				Server.get(context),
				feedID,
				stop,
				date,
				limit
			)

			if (result.error != null) {
				if (result.stream != null) {
					val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) }
					throw TrafficResponseException(result.error.statusCode, response.message, result.error)
				} else {
					throw TrafficResponseException(result.error.statusCode, "", result.error)
				}
			} else {
				when (val response =
					withContext(Dispatchers.IO) { DeparturesResponse.unmarshal(result.stream!!) }) {
					is DeparturesResponseDev -> StopEvents(
						response.departures.map { Event(it) },
						Stop(response.stop),
						response.alerts.map { Alert(it) })

					is DeparturesResponseV4 -> StopEvents(
						response.departures.map { Event(it) },
						Stop(response.stop),
						response.alerts.map { Alert(it) })

					is DeparturesResponseV3 -> StopEvents(
						response.departures.map { Event(it) },
						Stop(response.stop),
						response.alerts.map { Alert(it) })

					is DeparturesResponseV2 -> StopEvents(
						response.departures.map { Event(it) },
						Stop(response.stop),
						response.alerts.map { Alert(it) })

					is DeparturesResponseV1 -> StopEvents(
						response.departures.map { Event(it) },
						Stop(response.stop),
						response.alerts.map { Alert(it) })

					else -> null
				}
			}
		}
	}

	override suspend fun getLocatablesIn(
		context: Context,
		bl: Position,
		tr: Position,
	): List<Locatable>? {
		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) {
			val centre = Position(
				latitude = (bl.latitude + tr.latitude) / 2,
				longitude = (bl.longitude + tr.longitude) / 2
			)
			locateTransitousQueryables(
				centre, context, Metre(centre.distanceTo(bl).coerceAtMost(5000f).toDouble())
			).map {
				when (it) {
					is Stop -> it
					else -> null
				}
			}.filterNotNull()
		} else {
			null
		}
		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled()) {
			val result = xyz.apiote.bimba.czwek.api.getLocatablesIn(
				context,
				Server.get(context),
				PositionV1(bl.latitude, bl.longitude),
				PositionV1(tr.latitude, tr.longitude)
			)
			if (result.error != null) {
				if (result.stream != null) {
					val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) }
					throw TrafficResponseException(result.error.statusCode, response.message, result.error)
				} else {
					throw TrafficResponseException(result.error.statusCode, "", result.error)
				}
			} else {
				when (val response =
					withContext(Dispatchers.IO) { LocatablesResponse.unmarshal(result.stream!!) }) {
					is LocatablesResponseDev -> response.locatables.map {
						when (it) {
							is StopV3 -> Stop(it)
							is VehicleV3 -> Vehicle(it)
							else -> throw UnknownResourceException("locatables", it::class)
						}
					}

					is LocatablesResponseV3 -> response.locatables.map {
						when (it) {
							is StopV2 -> Stop(it)
							is VehicleV3 -> Vehicle(it)
							else -> throw UnknownResourceException("locatables", it::class)
						}
					}

					is LocatablesResponseV2 -> response.locatables.map {
						when (it) {
							is StopV2 -> Stop(it)
							is VehicleV2 -> Vehicle(it)
							else -> throw UnknownResourceException("locatables", it::class)
						}
					}

					is LocatablesResponseV1 -> response.locatables.map {
						when (it) {
							is StopV1 -> Stop(it)
							is VehicleV1 -> Vehicle(it)
							else -> throw UnknownResourceException("locatables", it::class)
						}
					}

					else -> null
				}
			}
		} else {
			null
		}
		return if (transitousQueryables == null && bimbaQueryables == null) {
			null
		} else {
			(bimbaQueryables ?: listOf()) + (transitousQueryables ?: listOf())
		}
	}

	override suspend fun getLine(
		context: Context, feedID: String, lineName: String, lineID: String
	): Line? {
		val result =
			xyz.apiote.bimba.czwek.api.getLine(context, Server.get(context), feedID, lineName, lineID)
		if (result.error != null) {
			if (result.stream != null) {
				val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) }
				throw TrafficResponseException(result.error.statusCode, response.message, result.error)
			} else {
				throw TrafficResponseException(result.error.statusCode, "", result.error)
			}
		} else {
			return when (val response =
				withContext(Dispatchers.IO) { LineResponse.unmarshal(result.stream!!) }) {
				is LineResponseDev -> Line(response.line)
				is LineResponseV1 -> Line(response.line)
				is LineResponseV2 -> Line(response.line)
				is LineResponseV3 -> Line(response.line)
				else -> null
			}
		}
	}

	override suspend fun queryQueryables(
		query: String, context: Context
	): List<Queryable>? {
		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) {
			getTransitousQueryables(query, context)
		} else {
			null
		}
		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled()) {
			getQueryables(query, null, context, "query")
		} else {
			null
		}
		return if (transitousQueryables == null && bimbaQueryables == null) {
			null
		} else {
			(bimbaQueryables ?: listOf()) + (transitousQueryables ?: listOf())
		}
	}

	override suspend fun locateQueryables(
		position: Position, context: Context
	): List<Queryable>? {
		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) {
			locateTransitousQueryables(position, context)
		} else {
			null
		}
		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled()) {
			getQueryables(null, position, context, "locate")
		} else {
			null
		}
		return if (transitousQueryables == null && bimbaQueryables == null) {
			null
		} else {
			(transitousQueryables ?: listOf()) + (bimbaQueryables ?: listOf()).sortedBy {
				it.location()?.distanceTo(position) ?: 0f
			}
		}
	}

	private suspend fun getQueryables(
		query: String?, position: Position?, context: Context, type: String
	): List<Queryable>? {
		val result = when (type) {
			"query" -> {
				xyz.apiote.bimba.czwek.api.queryQueryables(
					context,
					Server.get(context),
					query!!,
					limit = 12
				)
			}

			"locate" -> xyz.apiote.bimba.czwek.api.locateQueryables(
				context, Server.get(context), PositionV1(position!!.latitude, position.longitude)
			)

			else -> throw RuntimeException("Unknown query type $type")
		}
		if (result.error != null) {
			if (result.stream != null) {
				val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) }
				throw TrafficResponseException(result.error.statusCode, response.message, result.error)
			} else {
				throw TrafficResponseException(result.error.statusCode, "", result.error)
			}
		} else {
			return when (val response =
				withContext(Dispatchers.IO) { QueryablesResponse.unmarshal(result.stream!!) }) {
				is QueryablesResponseDev -> response.queryables.map {
					when (it) {
						is StopV3 -> Stop(it)
						is LineV3 -> Line(it)
						else -> throw UnknownResourceException("queryablesV4", it::class)
					}
				}

				is QueryablesResponseV1 -> response.queryables.map {
					when (it) {
						is StopV1 -> Stop(it)
						else -> throw UnknownResourceException("queryablesV1", it::class)
					}
				}

				is QueryablesResponseV2 -> response.queryables.map {
					when (it) {
						is StopV2 -> Stop(it)
						is LineV1 -> Line(it)
						else -> throw UnknownResourceException("queryablesV2", it::class)
					}
				}

				is QueryablesResponseV3 -> response.queryables.map {
					when (it) {
						is StopV2 -> Stop(it)
						is LineV2 -> Line(it)
						else -> throw UnknownResourceException("queryablesV3", it::class)
					}
				}

				is QueryablesResponseV4 -> response.queryables.map {
					when (it) {
						is StopV2 -> Stop(it)
						is LineV3 -> Line(it)
						else -> throw UnknownResourceException("queryablesV4", it::class)
					}
				}

				else -> null
			}
		}
	}
}
