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

package xyz.apiote.bimba.czwek.api

import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.MalformedURLException
import java.net.URL
import java.net.URLEncoder
import java.time.LocalDate
import java.time.format.DateTimeFormatter

// todo [3.2] constants
// todo [3.2] split api files to classes files

data class Server(
	val host: String,
	val token: String,
	val feeds: FeedsSettings,
	val apiPath: String
) {
	companion object {
		const val DEFAULT = "bimba.apiote.xyz"
		const val HOST_KEY = "host"
		const val TOKEN_KEY = "token"
		const val API_PATH_KEY = "apiPath"

		fun get(context: Context): Server {
			val preferences = context.getSharedPreferences("shp", MODE_PRIVATE)
			val apiPath = preferences.getString(API_PATH_KEY, "")!!
			val feeds = FeedsSettings.load(context, apiPath)
			val host = preferences.getString(HOST_KEY, DEFAULT)!!
			return Server(
				host, preferences.getString(TOKEN_KEY, "")!!,
				feeds, apiPath
			)
		}
	}
}

data class Result(val stream: InputStream?, val error: Error?)

data class Error(val statusCode: Int, val stringResource: Int, val imageResource: Int)

suspend fun getBimba(context: Context, server: Server): Result {
	return try {
		rawRequest(
			URL("${hostWithScheme(server.host)}/.well-known/traffic-api"), server, context, emptyArray()
		)
	} catch (e: MalformedURLException) {
		Result(null, Error(0, R.string.error_url, R.drawable.error_url))
	}
}

suspend fun getFeeds(context: Context, server: Server): Result {
	return try {
		rawRequest(
			URL("${server.apiPath}/"), server, context, arrayOf(1u, 2u)
		)
	} catch (_: MalformedURLException) {
		Result(null, Error(0, R.string.error_url, R.drawable.error_url))
	}
}

suspend fun queryQueryables(
	context: Context, server: Server, query: String, limit: Int? = null
): Result {
	val params = mutableMapOf("q" to query)
	if (limit != null) {
		params["limit"] = limit.toString()
	}
	return request(
		server,
		"queryables",
		null,
		params,
		context,
		arrayOf(1u, 2u, 3u, 4u),
		null
	)
}

suspend fun locateQueryables(context: Context, server: Server, near: PositionV1): Result {
	return request(
		server,
		"queryables",
		null,
		mapOf("near" to near.toString()),
		context,
		arrayOf(1u, 2u, 3u, 4u),
		null
	)
}

suspend fun getLocatablesIn(
	context: Context, server: Server, bl: PositionV1, tr: PositionV1
): Result {
	return request(
		server,
		"locatables",
		null,
		mapOf("lb" to bl.toString(), "rt" to tr.toString()),
		context,
		arrayOf(1u, 2u, 3u),
		null
	)
}

suspend fun getLine(
	context: Context,
	server: Server,
	feedID: String,
	lineName: String,
	lineID: String
): Result {
	return request(
		server,
		"lines",
		lineName,
		mapOf("line" to lineID),
		context,
		arrayOf(1u, 2u, 3u),
		feedID
	)
}

suspend fun getDepartures(
	context: Context,
	server: Server,
	feedID: String,
	stop: String,
	date: LocalDate?,
	limit: Int? = null
): Result {
	val params = mutableMapOf("code" to stop)
	if (date != null) {
		params["date"] = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
	}
	if (limit != null) {
		params["limit"] = limit.toString(10)
	}
	return request(
		server,
		"departures",
		null,
		params,
		context,
		arrayOf(1u, 2u, 3u, 4u),
		feedID
	)
}

fun mapHttpError(code: Int): Pair<Int, Int> {
	return when (code) {
		41 -> Pair(R.string.error_41, R.drawable.error_locality)
		44 -> Pair(R.string.error_44, R.drawable.error_departures)
		400 -> Pair(R.string.error_400, R.drawable.error_app)
		401 -> Pair(R.string.error_401, R.drawable.error_sec)
		403 -> Pair(R.string.error_403, R.drawable.error_sec)
		404 -> Pair(R.string.error_404, R.drawable.error_search)
		406 -> Pair(R.string.error_406, R.drawable.error_accept)
		429 -> Pair(R.string.error_429, R.drawable.error_limit)
		500 -> Pair(R.string.error_50x, R.drawable.error_server)
		502 -> Pair(R.string.error_50x, R.drawable.error_server)
		503 -> Pair(R.string.error_50x, R.drawable.error_server)
		504 -> Pair(R.string.error_50x, R.drawable.error_server)
		else -> Pair(R.string.error_unknown, R.drawable.error_other)
	}
}

suspend fun rawRequest(
	url: URL, server: Server, context: Context, responseVersion: Array<UInt>
): Result {
	if (!isNetworkAvailable(context)) {
		return Result(null, Error(0, R.string.error_offline, R.drawable.error_net))
	}
	return withContext(Dispatchers.IO) {
		val c = (url.openConnection() as HttpURLConnection).apply {
			setRequestProperty(
				"User-Agent",
				"${context.getString(R.string.applicationId)}/${context.getString(R.string.versionName)} (${Build.VERSION.SDK_INT})"
			)
			setRequestProperty("X-Bimba-Token", server.token)
			responseVersion.forEach { addRequestProperty("Accept", "application/$it+bare") }
		}
		try {
			if (c.responseCode == 200) {
				Result(c.inputStream, null)
			} else {
				val (string, image) = mapHttpError(c.responseCode)
				Result(c.errorStream, Error(c.responseCode, string, image))
			}
		} catch (e: IOException) {
			Result(null, Error(0, R.string.error_connecting, R.drawable.error_server))
		}
	}
}

internal fun isNetworkAvailable(context: Context): Boolean {
	val connectivityManager =
		context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

	return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
		connectivityManager.activeNetwork?.let { network ->
			connectivityManager.getNetworkCapabilities(network)?.let { capabilities ->
				capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || capabilities.hasTransport(
					NetworkCapabilities.TRANSPORT_CELLULAR
				) || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
			}
		} ?: false
	} else {
		@Suppress("DEPRECATION")
		connectivityManager.activeNetworkInfo?.isConnected ?: false
	}
}

suspend fun request(
	server: Server,
	resource: String,
	item: String?,
	params: Map<String, String>,
	context: Context,
	responseVersion: Array<UInt>,
	feeds: String?
): Result {
	return withContext(Dispatchers.IO) {
		val url = URL( // todo [3.2] scheme, host, path, constructed query
			"${server.apiPath}/${feeds?.ifEmpty { server.feeds.getIDs() } ?: server.feeds.getIDs()}/$resource${
				if (item == null) {
					""
				} else {
					"/${URLEncoder.encode(item, "utf-8")}"
				}
			}${
				params.map {
					"${it.key}=${
						URLEncoder.encode(
							it.value, "utf-8"
						)
					}"
				}.joinToString("&", "?")
			}"
		)
		rawRequest(url, server, context, responseVersion)
	}
}

fun hostWithScheme(host: String): String =
	if (host.startsWith("http://") or host.startsWith("https://")) {
		host
	} else {
		"https://$host"
	}
