package jp.juggler.subwaytooter.api.auth

import android.net.Uri
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.auth.AuthMisskey10.Companion.encodeScopeArray
import jp.juggler.subwaytooter.api.auth.AuthMisskey10.Companion.getScopeArrayMisskey
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.FcmFlavor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import java.util.UUID

/**
 * miauth と呼ばれている認証手順。
 * STではMisskey 13から使用する。
 */
class AuthMisskey13(override val client: TootApiClient) : AuthBase() {
    companion object {
        private val log = LogCategory("MisskeyMiAuth")
        private const val appIconUrl = "https://m1j.zzz.ac/subwaytooter-miauth-icon.png"
        private const val callbackUrl = "${FcmFlavor.CUSTOM_SCHEME}://miauth/auth_callback"

        fun isCallbackUrl(uriStr: String) = uriStr.startsWith(callbackUrl)
    }

    private val api10 = ApiAuthMisskey10(client)
    private val api13 = ApiAuthMisskey13(client)

    // 認証されたアカウントのユーザ情報を取得する
    override suspend fun verifyAccount(
        accessToken: String,
        outTokenJson: JsonObject?,
        misskeyVersion: Int,
    ): JsonObject = api10.verifyAccount(
        apiHost = apiHost ?: error("missing apiHost"),
        accessToken = accessToken,
    ).also {
        // ユーザ情報を読めたならtokenInfoを保存する
        outTokenJson?.apply {
            put(KEY_AUTH_VERSION, AUTH_VERSION)
            put(KEY_API_KEY_MISSKEY, accessToken)
            put(KEY_MISSKEY_VERSION, misskeyVersion)
        }
    }

    /**
     * クライアントを登録してブラウザで開くURLを生成する
     * 成功したら TootApiResult.data にURL文字列を格納すること
     */
    override suspend fun authStep1(
        ti: TootInstance?,
        forceUpdateClient: Boolean,
    ): Uri {
        if (!PrefB.bpEnableDeprecatedSomething.value) {
            error(context.getString(R.string.misskey_support_end))
        }

        val apiHost = apiHost ?: error("missing apiHost")

        val sessionId = UUID.randomUUID().toString()

        client.context.prefDevice.saveLastAuth(
            host = apiHost.ascii,
            secret = sessionId,
            dbId = account?.db_id,
        )

        return api13.createAuthUrl(
            apiHost = apiHost,
            clientName = clientName,
            iconUrl = appIconUrl,
            callbackUrl = callbackUrl,
            permission = getScopeArrayMisskey(ti).encodeScopeArray(),
            sessionId = sessionId,
        )
    }

    override suspend fun authStep2(uri: Uri): Auth2Result {

        // 認証開始時に保存した情報
        val prefDevice = client.context.prefDevice
        val savedSessionId = prefDevice.lastAuthSecret

        val apiHost = prefDevice.lastAuthInstance
            ?.let { Host.parse(it) }
            ?: error("missing apiHost")

        when (val dbId = prefDevice.lastAuthDbId) {
            // new registration
            null -> client.apiHost = apiHost

            // update access token
            else -> {
                val sa = daoSavedAccount.loadAccount(dbId)
                    ?: error("missing account db_id=$dbId")
                client.account = sa
            }
        }

        // コールバックURLに含まれるセッションID
        val sessionId = uri.getQueryParameter("session").notEmpty()
            ?: error("missing sessionId in callback URL")

        if (sessionId != savedSessionId) {
            error("auth session id not match.")
        }

        val ti = TootInstance.getOrThrow(client)

        val misskeyVersion = ti.misskeyVersionMajor

        val data = api13.checkAuthSession(apiHost, sessionId)

        val ok = data.boolean("ok")
        if (ok != true) {
            error("Authentication result is not ok. [$ok]")
        }

        val apiKey = data.string("token")
            ?: error("missing token.")
        log.i("apiKey=$apiKey")

        val accountJson = data.jsonObject("user")
            ?: error("missing user.")

        val user = TootParser(context, linkHelper = LinkHelper.create(ti))
            .account(accountJson)
            ?: error("can't parse user json.")

        prefDevice.removeLastAuth()

        return Auth2Result(
            tootInstance = ti,
            tokenJson = JsonObject().apply {
                user.id.putTo(this, KEY_USER_ID)
                put(KEY_MISSKEY_VERSION, misskeyVersion)
                put(KEY_AUTH_VERSION, AUTH_VERSION)
                put(KEY_API_KEY_MISSKEY, apiKey)
            },
            accountJson = accountJson,
            tootAccount = user,
        )
    }
}
