package xyz.apiote.bimba.czwek.api

import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.net.ConnectivityManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import xyz.apiote.bimba.czwek.R
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.MalformedURLException
import java.net.URL
import java.net.URLEncoder

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

data class Server(val host: String, val token: String, val feeds: String, val apiPath: String) {
	companion object {
		fun get(context: Context): Server {
			val preferences = context.getSharedPreferences("shp", MODE_PRIVATE)
			val apiPath = preferences.getString("apiPath", "")!!
			val feeds = context.getSharedPreferences(
				URLEncoder.encode(apiPath, "utf-8"),
				MODE_PRIVATE
			).all.filter { it.value as Boolean }.keys.joinToString(",")
			val host = preferences.getString("host", "bimba.apiote.xyz")!!
			return Server(
				host, preferences.getString("token", "")!!,
				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(cm: ConnectivityManager, server: Server): Result {
	return try {
		rawRequest(
			URL("${hostWithScheme(server.host)}/.well-known/traffic-api"), server, cm, emptyArray()
		)
	} catch (e: MalformedURLException) {
		Result(null, Error(0, R.string.error_url, R.drawable.error_url))
	}
}

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

suspend fun queryQueryables(
	cm: ConnectivityManager, 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, cm, arrayOf(1u, 2u), null)
}

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

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

suspend fun getLine(cm: ConnectivityManager, server: Server, feedID: String, line: String): Result {
	return request(server, "lines", line, mapOf(), cm, arrayOf(1u), feedID)
}

suspend fun getDepartures(
	cm: ConnectivityManager, server: Server, feedID: String, stop: String, line: String? = null
): Result {
	val params = mutableMapOf("code" to stop)
	if (line != null) {
		params["line"] = line
	}
	return request(server, "departures", null, params, cm, arrayOf(1u, 2u), feedID)
}

suspend fun rawRequest(
	url: URL, server: Server, cm: ConnectivityManager, responseVersion: Array<UInt>
): Result {
	@Suppress("DEPRECATION")  // fixme later(API_29, API_23) https://developer.android.com/reference/android/net/ConnectivityManager#getActiveNetwork()
	if (cm.activeNetworkInfo == null) {
		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("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) = when (c.responseCode) {
					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)
					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)
				}
				Result(c.errorStream, Error(c.responseCode, string, image))
			}
		} catch (e: IOException) {
			Result(null, Error(0, R.string.error_connecting, R.drawable.error_server))
		}
	}
}

suspend fun request(
	server: Server,
	resource: String,
	item: String?,
	params: Map<String, String>,
	cm: ConnectivityManager,
	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 } ?: server.feeds}/$resource${
				if (item == null) {
					""
				} else {
					"/$item"
				}
			}${
				params.map {
					"${it.key}=${
						URLEncoder.encode(
							it.value, "utf-8"
						)
					}"
				}.joinToString("&", "?")
			}"
		)
		rawRequest(url, server, cm, responseVersion)
	}
}

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