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

package xyz.apiote.bimba.czwek.api

import android.content.Context
import android.util.JsonReader
import android.util.JsonToken
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.api.responses.ErrorResponse
import xyz.apiote.bimba.czwek.repo.Colour
import xyz.apiote.bimba.czwek.repo.CongestionLevel
import xyz.apiote.bimba.czwek.repo.Event
import xyz.apiote.bimba.czwek.repo.LineStub
import xyz.apiote.bimba.czwek.repo.LineType
import xyz.apiote.bimba.czwek.repo.OccupancyStatus
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.repo.StopEvents
import xyz.apiote.bimba.czwek.repo.TrafficResponseException
import xyz.apiote.bimba.czwek.repo.Vehicle
import xyz.apiote.bimba.czwek.units.Mps
import java.security.MessageDigest
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime

@OptIn(ExperimentalStdlibApi::class)
suspend fun getTransitousDepartures(
	context: Context,
	stop: String,
	date: LocalDate?,
	limit: Int?
): StopEvents {
	if (!isNetworkAvailable(context)) {
		throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net))
	}

	// TODO shouldn't it be start-of-day in stop's timezone?
	val timestamp =
		date?.atStartOfDay(ZoneId.systemDefault())?.toEpochSecond() ?: ZonedDateTime.now()
			.toEpochSecond()

	val json = """
		{
			"destination": {
				"type": "Module",
				"target": "/railviz/get_station"
			},
			"content_type": "RailVizStationRequest",
			"content": {
				"station_id": "$stop",
				"time": $timestamp,
				"event_count": ${limit ?: 12},
				"direction": "LATER",
				"by_schedule_time": false
			}
		}
	"""

	val result = askTransitous(context, json)

	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 withContext(Dispatchers.IO) {
			val events = mutableListOf<Event>()
			var stopID = ""
			var stopName = ""
			var latitude = 0.0
			var longitude = 0.0
			val r = JsonReader(result.stream!!.bufferedReader())
			r.withObject { name ->
				if (name != "content") {
					r.skipValue()
					return@withObject
				}
				r.withObject { name ->
					when (name) {
						"events" -> {
							r.withArray {
								var reason = ""
								var eventTimestamp: Long = 0
								var valid = false
								var tripID = ""
								var idLineID = ""
								var stationID = ""
								var targetStationID = ""
								var targetTimestamp = 0
								var idTimestamp = 0
								var trainNr = 0
								var clasz = -1
								var direction = ""
								var lineID = ""
								var lineName = ""
								var lineColour = ""
								var eventType = ""
								r.withObject { name ->
									when (name) {
										"event" -> {
											r.withObject { name ->
												when (name) {
													"reason" -> reason = r.nextString()
													"schedule_time" -> r.skipValue()
													"schedule_track" -> r.skipValue()
													"time" -> eventTimestamp = r.nextLong()
													"track" -> r.skipValue()
													"valid" -> valid = r.nextBoolean()
													else -> r.skipValue()
												}
											}
										}

										"trips" -> {
											r.withArray { i ->
												if (i == 0) {
													r.withObject { name ->
														when (name) {
															"id" -> {
																r.withObject { name ->
																	when (name) {
																		"id" -> tripID = r.nextString()
																		"line_id" -> idLineID = r.nextString()
																		"station_id" -> stationID = r.nextString()
																		"target_station_id" -> targetStationID = r.nextString()
																		"target_time" -> targetTimestamp = r.nextInt()
																		"time" -> idTimestamp = r.nextInt()
																		"train_nr" -> trainNr = r.nextInt()
																		else -> r.skipValue()
																	}
																}
															}

															"transport" -> {
																r.withObject { name ->
																	Log.d("WithObject", "next is ${name}")
																	when (name) {
																		"clasz" -> clasz = r.nextInt()
																		"direction" -> direction = r.nextString()
																		"line_id" -> lineID = r.nextString()
																		"name" -> lineName = r.nextString()
																		"provider" -> r.skipValue()
																		"provider_url" -> r.skipValue()
																		"range" -> r.skipValue()
																		"route_color" -> lineColour = r.nextString()
																		"route_text_color" -> r.skipValue()
																		else -> r.skipValue()
																	}
																}
															}
															else -> r.skipValue()
														}
													}
												} else {
													r.skipValue()
												}
											}
										}

										"type" -> {
											eventType = r.nextString()
										}
										else -> r.skipValue()
									}
								}
								if (eventType == "ARR") {
									val hash = MessageDigest.getInstance("SHA-256").let {
										it.update(tripID.toByteArray())
										it.update(idLineID.toByteArray())
										it.update(stationID.toByteArray())
										it.update(targetStationID.toByteArray())
										it.update(targetTimestamp.toHexString().toByteArray())
										it.update(idTimestamp.toHexString().toByteArray())
										it.update(trainNr.toHexString().toByteArray())
										it.digest()
									}.toHexString()
									val t =
										ZonedDateTime.ofInstant(
											Instant.ofEpochSecond(eventTimestamp),
											ZoneId.systemDefault()
										)
									events.add(
										Event(
											id = hash,
											arrivalTime = Time(
												t.hour.toUInt(),
												t.minute.toUInt(),
												t.second.toUInt(),
												(t.dayOfYear - ZonedDateTime.now().dayOfYear).toByte(),
												ZoneId.systemDefault().id
											),
											departureTime = Time(
												t.hour.toUInt(),
												t.minute.toUInt(),
												t.second.toUInt(),
												(t.dayOfYear - ZonedDateTime.now().dayOfYear).toByte(),
												ZoneId.systemDefault().id
											),
											status = 0u,
											isRealtime = reason in setOf("IS", "PROPAGATION", "FORECAST"),
											vehicle = Vehicle(
												"",
												Position(0.0, 0.0),
												0u,
												Mps(0),
												LineStub(
													lineName,
													LineType.fromTransitous(clasz),
													Colour.fromHex(lineColour)
												),
												direction,
												CongestionLevel.UNKNOWN,
												OccupancyStatus.UNKNOWN
											),
											boarding = 0xffu,
											alerts = emptyList(),
											exact = true,
											terminusArrival = !valid
										)
									)
								}
							}
						}

						"station" -> {
							r.withObject { name ->
								when (name) {
									"id" -> stopID = r.nextString()
									"name" -> stopName = r.nextString()
									"pos" -> {
										r.withObject { posName ->
											when (posName) {
												"lat" -> latitude = r.nextDouble()
												"lng" -> longitude = r.nextDouble()
											}
										}
									}
								}
							}
						}
						else -> r.skipValue()
					}
				}
			}

			return@withContext StopEvents(
				events,
				Stop(stopID, stopName, stopName, "", "transitous", Position(latitude, longitude), listOf()),
				listOf()
			)
		}
	}
}

fun JsonReader.withObject(f: (String) -> Unit) {
	beginObject()
	while (hasNext()) {
		try {
			f(nextName())
		} catch (e: IllegalStateException) {
			val a: JsonToken = this.peek()
			Log.d("WithObject", "next is ${a.name} ${nextString()}")
			throw e
		}
	}
	endObject()
}

fun JsonReader.withArray(f: (Int) -> Unit) {
	beginArray()
	var i = 0
	while (hasNext()) {
		f(i)
		i++
	}
	endArray()
}
