package jp.juggler.subwaytooter.table

import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.provider.BaseColumns
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.auth.Auth2Result
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.notification.checkNotificationImmediate
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
import jp.juggler.subwaytooter.pref.lazyContext
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.errorEx
import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption
import jp.juggler.util.media.MovieResizeMode
import jp.juggler.util.media.MovieResizeConfig
import jp.juggler.util.media.ResizeConfig
import jp.juggler.util.media.ResizeType
import kotlin.math.max

/**
 * ユーザが操作するアカウント
 * - Access.saveNew で insert。直後にロードし直す
 * - 通知の更新状況などは AccountNotificationStatus に保存するようになった
 */
class SavedAccount(
    val db_id: Long,

    acctArg: String,
    apiHostArg: String? = null,
    apDomainArg: String? = null,

    var accountJson: JsonObject? = null,
    var confirmBoost: Boolean = false,
    var confirmFavourite: Boolean = false,
    var confirmFollow: Boolean = false,
    var confirmFollowLocked: Boolean = false,
    var confirmPost: Boolean = false,
    var confirmReaction: Boolean = true,
    var confirmUnbookmark: Boolean = true,
    var confirmUnboost: Boolean = false,
    var confirmUnfavourite: Boolean = false,
    var confirmUnfollow: Boolean = false,
    var defaultSensitive: Boolean = false,
    var defaultText: String = "",
    var dontHideNsfw: Boolean = false,
    var dontShowTimeout: Boolean = false,
    var expandCw: Boolean = false,
    var extraJson: JsonObject = JsonObject(),
    var imageMaxMegabytes: String? = null,
    var imageResize: String? = null,
    var loginAccount: TootAccount? = null, // 疑似アカウントではnull
    var maxTootChars: Int = 0,
    var movieMaxMegabytes: String? = null,
    var notificationBoost: Boolean = false,
    var notificationFavourite: Boolean = false,
    var notificationFollow: Boolean = false,
    var notificationFollowRequest: Boolean = false,
    var notificationMention: Boolean = false,
    var notificationPost: Boolean = false,
    var notificationReaction: Boolean = false,
    var notificationUpdate: Boolean = true,
    var notificationVote: Boolean = false,
    var pushPolicy: String? = null,
    var tokenJson: JsonObject? = null,
    var visibility: TootVisibility = TootVisibility.Public,
//    var soundUri: String = "",
// private var lastNotificationError: String? = null,
// private var last_push_endpoint: String? = null,
// private var last_subscription_error: String? = null,
// private var register_key: String? = null
// private var register_time: Long = 0
// var notification_tag: String? = null

    override var misskeyVersion: Int = 0,
) : LinkHelper {

    // SavedAccountのロード時にhostを供給する必要があった
    override val apiHost: Host
    override val apDomain: Host

    val username: String
    val acct: Acct

    private val jsonDelegates = JsonDelegates { extraJson }

    @JsonPropInt("movieTranscodeMode", 0)
    var movieTranscodeMode by jsonDelegates.int

    @JsonPropString("movieTranscodeBitrate", "2000000")
    var movieTranscodeBitrate by jsonDelegates.string

    @JsonPropString("movieTranscodeFramerate", "30")
    var movieTranscodeFramerate by jsonDelegates.string

    @JsonPropString("movieTranscodeSquarePixels", "2304000")
    var movieTranscodeSquarePixels by jsonDelegates.string

    @JsonPropString("lang2", LANG_WEB)
    var lang by jsonDelegates.string

    @JsonPropBoolean("notification_status_reference", true)
    var notificationStatusReference by jsonDelegates.boolean

    @JsonPropBoolean("notificationPushEnable", true)
    var notificationPushEnable by jsonDelegates.boolean

    @JsonPropBoolean("notificationPullEnable", false)
    var notificationPullEnable by jsonDelegates.boolean

    @JsonPropInt("notificationAccentColor", 0)
    var notificationAccentColor by jsonDelegates.int

    init {
        // log.i("ctor acctArg $acctArg")

        // acctArg はMastodonの生のやつで、ドメイン部分がない場合がある
        // Acct.parse はHost部分がnullのacctになるかもしれない
        val tmpAcct = Acct.parse(acctArg)
        this.username = tmpAcct.username
        if (username.isEmpty()) error("missing username in acct")

        val tmpApiHost = apiHostArg?.notEmpty()?.let { Host.parse(it) }
        val tmpApDomain = apDomainArg?.notEmpty()?.let { Host.parse(it) }

        this.apiHost = tmpApiHost ?: tmpApDomain ?: tmpAcct.host ?: error("missing apiHost")
        this.apDomain = tmpApDomain ?: tmpApiHost ?: tmpAcct.host ?: error("missing apDomain")

        // Full Acct
        this.acct = tmpAcct.followHost(apDomain)
    }

    constructor(context: Context, cursor: Cursor) : this(
        db_id = cursor.getLong(COL_ID), // db_id
        acctArg = cursor.getString(COL_USER), // acct
        apiHostArg = cursor.getStringOrNull(COL_HOST), // host
        apDomainArg = cursor.getStringOrNull(COL_DOMAIN), // host

        accountJson = cursor.getStringOrNull(COL_ACCOUNT)?.decodeJsonObject(),
        confirmBoost = cursor.getBoolean(COL_CONFIRM_BOOST),
        confirmFavourite = cursor.getBoolean(COL_CONFIRM_FAVOURITE),
        confirmFollow = cursor.getBoolean(COL_CONFIRM_FOLLOW),
        confirmFollowLocked = cursor.getBoolean(COL_CONFIRM_FOLLOW_LOCKED),
        confirmPost = cursor.getBoolean(COL_CONFIRM_POST),
        confirmReaction = cursor.getBoolean(COL_CONFIRM_REACTION),
        confirmUnbookmark = cursor.getBoolean(COL_CONFIRM_UNBOOKMARK),
        confirmUnboost = cursor.getBoolean(COL_CONFIRM_UNBOOST),
        confirmUnfavourite = cursor.getBoolean(COL_CONFIRM_UNFAVOURITE),
        confirmUnfollow = cursor.getBoolean(COL_CONFIRM_UNFOLLOW),
        defaultSensitive = cursor.getBoolean(COL_DEFAULT_SENSITIVE),
        defaultText = cursor.getStringOrNull(COL_DEFAULT_TEXT) ?: "",
        dontHideNsfw = cursor.getBoolean(COL_DONT_HIDE_NSFW),
        dontShowTimeout = cursor.getBoolean(COL_DONT_SHOW_TIMEOUT),
        expandCw = cursor.getBoolean(COL_EXPAND_CW),
        extraJson = cursor.getStringOrNull(COL_EXTRA_JSON)?.decodeJsonObject() ?: JsonObject(),
        imageMaxMegabytes = cursor.getStringOrNull(COL_IMAGE_MAX_MEGABYTES),
        imageResize = cursor.getStringOrNull(COL_IMAGE_RESIZE),
        maxTootChars = cursor.getInt(COL_MAX_TOOT_CHARS),
        movieMaxMegabytes = cursor.getStringOrNull(COL_MOVIE_MAX_MEGABYTES),
        notificationBoost = cursor.getBoolean(COL_NOTIFICATION_BOOST),
        notificationFavourite = cursor.getBoolean(COL_NOTIFICATION_FAVOURITE),
        notificationFollow = cursor.getBoolean(COL_NOTIFICATION_FOLLOW),
        notificationFollowRequest = cursor.getBoolean(COL_NOTIFICATION_FOLLOW_REQUEST),
        notificationMention = cursor.getBoolean(COL_NOTIFICATION_MENTION),
        notificationPost = cursor.getBoolean(COL_NOTIFICATION_POST),
        notificationReaction = cursor.getBoolean(COL_NOTIFICATION_REACTION),
        notificationUpdate = cursor.getBoolean(COL_NOTIFICATION_UPDATE),
        notificationVote = cursor.getBoolean(COL_NOTIFICATION_VOTE),
        pushPolicy = cursor.getStringOrNull(COL_PUSH_POLICY),
//        soundUri = cursor.getString(COL_SOUND_URI),
        tokenJson = cursor.getString(COL_TOKEN).decodeJsonObject(),
        visibility = TootVisibility.parseSavedVisibility(cursor.getStringOrNull(COL_VISIBILITY))
            ?: TootVisibility.Public,

        misskeyVersion = cursor.getInt(COL_MISSKEY_VERSION),

        //        lastNotificationError = cursor.getStringOrNull(COL_LAST_NOTIFICATION_ERROR)
        //        last_push_endpoint = cursor.getStringOrNull(COL_LAST_PUSH_ENDPOINT)
        //        last_subscription_error = cursor.getStringOrNull(COL_LAST_SUBSCRIPTION_ERROR)
        //        notification_tag = cursor.getStringOrNull(COL_NOTIFICATION_TAG)
        //        register_key = cursor.getStringOrNull(COL_REGISTER_KEY)
        //        register_time = cursor.getLong(COL_REGISTER_TIME)

    ) {
        loginAccount = if (accountJson?.get("id") == null) {
            null // 疑似アカウント
        } else {
            TootParser(
                context,
                LinkHelper.create(
                    apiHostArg = this@SavedAccount.apiHost,
                    apDomainArg = this@SavedAccount.apDomain,
                    misskeyVersion = misskeyVersion
                )
            ).account(accountJson)
                ?: error("missing loginAccount for $accountJson")
        }
    }

    companion object : TableCompanion {

        private val log = LogCategory("SavedAccount")
        override val table = "access_info"

        private const val COL_ID = BaseColumns._ID
        private const val COL_HOST = "h"
        private const val COL_DOMAIN = "d"
        private const val COL_USER = "u"
        private const val COL_ACCOUNT = "a"
        private const val COL_TOKEN = "t"
        private const val COL_CONFIRM_BOOST = "confirm_boost"
        private const val COL_CONFIRM_FAVOURITE = "confirm_favourite"
        private const val COL_CONFIRM_FOLLOW = "confirm_follow"
        private const val COL_CONFIRM_FOLLOW_LOCKED = "confirm_follow_locked"
        private const val COL_CONFIRM_POST = "confirm_post"
        private const val COL_CONFIRM_REACTION = "confirm_reaction"
        private const val COL_CONFIRM_UNBOOKMARK = "confirm_unbookmark"
        private const val COL_CONFIRM_UNBOOST = "confirm_unboost"
        private const val COL_CONFIRM_UNFAVOURITE = "confirm_unfavourite"
        private const val COL_CONFIRM_UNFOLLOW = "confirm_unfollow"
        private const val COL_DEFAULT_SENSITIVE = "default_sensitive"
        private const val COL_DEFAULT_TEXT = "default_text"
        private const val COL_DONT_HIDE_NSFW = "dont_hide_nsfw"
        private const val COL_DONT_SHOW_TIMEOUT = "dont_show_timeout"
        private const val COL_EXPAND_CW = "expand_cw"
        private const val COL_EXTRA_JSON = "extra_json"
        private const val COL_IMAGE_MAX_MEGABYTES = "image_max_megabytes"
        private const val COL_IMAGE_RESIZE = "image_resize"
        private const val COL_MAX_TOOT_CHARS = "max_toot_chars"
        private const val COL_MISSKEY_VERSION = "is_misskey"
        private const val COL_MOVIE_MAX_MEGABYTES = "movie_max_megabytes"
        private const val COL_NOTIFICATION_BOOST = "notification_boost"
        private const val COL_NOTIFICATION_FAVOURITE = "notification_favourite"
        private const val COL_NOTIFICATION_FOLLOW = "notification_follow"
        private const val COL_NOTIFICATION_FOLLOW_REQUEST = "notification_follow_request"
        private const val COL_NOTIFICATION_MENTION = "notification_mention"
        private const val COL_NOTIFICATION_POST = "notification_post"
        private const val COL_NOTIFICATION_REACTION = "notification_reaction"
        private const val COL_NOTIFICATION_TAG = "notification_server"
        private const val COL_NOTIFICATION_UPDATE = "notification_update"
        private const val COL_NOTIFICATION_VOTE = "notification_vote"
        private const val COL_PUSH_POLICY = "push_policy"
        private const val COL_VISIBILITY = "visibility"
//        private const val COL_SOUND_URI = "sound_uri"
//        private const val COL_LAST_NOTIFICATION_ERROR = "last_notification_error"
//        private const val COL_LAST_PUSH_ENDPOINT = "last_push_endpoint"
//        private const val COL_LAST_SUBSCRIPTION_ERROR = "last_subscription_error"
//        private const val COL_REGISTER_KEY = "register_key"
//        private const val COL_REGISTER_TIME = "register_time"

        // COL_MISSKEY_VERSIONのカラム名がおかしいのは、昔はboolean扱いだったから
        // 0: not misskey
        // 1: old(v10) misskey
        // 11: misskey v11

        val columnList = MetaColumns(table, 0).apply {
            column(0, COL_ID, "INTEGER PRIMARY KEY")
            column(0, COL_ACCOUNT, "text not null")
            column(0, COL_CONFIRM_BOOST, MetaColumns.TS_TRUE)
            column(0, COL_DONT_HIDE_NSFW, MetaColumns.TS_ZERO)
            column(0, COL_HOST, "text not null")
            column(0, COL_TOKEN, "text not null")
            column(0, COL_USER, "text not null")
            column(0, COL_VISIBILITY, "text")
            column(2, COL_NOTIFICATION_BOOST, MetaColumns.TS_TRUE)
            column(2, COL_NOTIFICATION_FAVOURITE, MetaColumns.TS_TRUE)
            column(2, COL_NOTIFICATION_FOLLOW, MetaColumns.TS_TRUE)
            column(2, COL_NOTIFICATION_MENTION, MetaColumns.TS_TRUE)
            column(10, COL_CONFIRM_FOLLOW, MetaColumns.TS_TRUE)
            column(10, COL_CONFIRM_FOLLOW_LOCKED, MetaColumns.TS_TRUE)
            column(10, COL_CONFIRM_POST, MetaColumns.TS_TRUE)
            column(10, COL_CONFIRM_UNFOLLOW, MetaColumns.TS_TRUE)
            column(13, COL_NOTIFICATION_TAG, MetaColumns.TS_EMPTY)
//            column(14, COL_REGISTER_KEY, MetaColumns.TS_EMPTY)
//            column(14, COL_REGISTER_TIME, MetaColumns.TS_ZERO)
//            column(16, COL_SOUND_URI, MetaColumns.TS_EMPTY)
            column(18, COL_DONT_SHOW_TIMEOUT, MetaColumns.TS_ZERO)
            column(23, COL_CONFIRM_FAVOURITE, MetaColumns.TS_TRUE)
            column(24, COL_CONFIRM_UNBOOST, MetaColumns.TS_TRUE)
            column(24, COL_CONFIRM_UNFAVOURITE, MetaColumns.TS_TRUE)
            column(27, COL_DEFAULT_TEXT, MetaColumns.TS_EMPTY)
            column(28, COL_MISSKEY_VERSION, MetaColumns.TS_ZERO)
            column(33, COL_NOTIFICATION_REACTION, MetaColumns.TS_TRUE)
            column(33, COL_NOTIFICATION_VOTE, MetaColumns.TS_TRUE)
            column(38, COL_DEFAULT_SENSITIVE, MetaColumns.TS_ZERO)
            column(38, COL_EXPAND_CW, MetaColumns.TS_ZERO)
            column(39, COL_MAX_TOOT_CHARS, MetaColumns.TS_ZERO)
//            column(42, COL_LAST_NOTIFICATION_ERROR, "text")
            column(44, COL_NOTIFICATION_FOLLOW_REQUEST, MetaColumns.TS_TRUE)
//            column(45, COL_LAST_SUBSCRIPTION_ERROR, "text")
//            column(46, COL_LAST_PUSH_ENDPOINT, "text")
            column(56, COL_DOMAIN, "text")
            column(57, COL_NOTIFICATION_POST, MetaColumns.TS_TRUE)
            column(59, COL_IMAGE_MAX_MEGABYTES, "text default null")
            column(59, COL_IMAGE_RESIZE, "text default null")
            column(59, COL_MOVIE_MAX_MEGABYTES, "text default null")
            column(60, COL_PUSH_POLICY, "text default null")
            column(61, COL_CONFIRM_REACTION, MetaColumns.TS_TRUE)
            column(62, COL_CONFIRM_UNBOOKMARK, MetaColumns.TS_TRUE)
            column(63, COL_EXTRA_JSON, "text default null")
            column(64, COL_NOTIFICATION_UPDATE, MetaColumns.TS_TRUE)
            createExtra = {
                arrayOf(
                    "create index if not exists ${table}_user on $table(u)",
                    "create index if not exists ${table}_host on $table(h,u)"
                )
            }
        }

        // アプリデータのインポート時に呼ばれる
        fun onDBDelete(db: SQLiteDatabase) {
            try {
                db.execSQL("drop table if exists $table")
            } catch (ex: Throwable) {
                log.e(ex, "can't delete table $table.")
            }
        }

        override fun onDBCreate(db: SQLiteDatabase) =
            columnList.onDBCreate(db)

        override fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) =
            columnList.onDBUpgrade(db, oldVersion, newVersion)

        /////////////////////////////////

        val defaultResizeConfig = ResizeConfig(ResizeType.LongSide, 1280)

        internal val resizeConfigList = arrayOf(
            ResizeConfig(ResizeType.None, 0),

            ResizeConfig(ResizeType.LongSide, 640),
            ResizeConfig(ResizeType.LongSide, 800),
            ResizeConfig(ResizeType.LongSide, 1024),
            ResizeConfig(ResizeType.LongSide, 1280),
            ResizeConfig(ResizeType.LongSide, 1600),
            ResizeConfig(ResizeType.LongSide, 2048),

            ResizeConfig(ResizeType.SquarePixel, 640),
            ResizeConfig(ResizeType.SquarePixel, 800),
            ResizeConfig(ResizeType.SquarePixel, 1024),
            ResizeConfig(ResizeType.SquarePixel, 1280),
            ResizeConfig(ResizeType.SquarePixel, 1440, R.string.size_1920_1080),
            ResizeConfig(ResizeType.SquarePixel, 1600),
            ResizeConfig(ResizeType.SquarePixel, 2048)
        )

        const val LANG_WEB = "(web)"
        const val LANG_DEVICE = "(device)"

        // 横断検索用の、何とも紐ついていないアカウント
        // 保存しない。
        val na by lazy {
            SavedAccount(-1L, "?@?").apply {
                notificationFollow = false
                notificationFollowRequest = false
                notificationFavourite = false
                notificationBoost = false
                notificationMention = false
                notificationReaction = false
                notificationVote = false
                notificationPost = false
                notificationUpdate = false
            }
        }

        private fun parse(context: Context, cursor: Cursor) =
            try {
                SavedAccount(context, cursor)
            } catch (ex: Throwable) {
                log.e(ex, "parse failed.")
                null
            }

        private fun Boolean.booleanToInt(trueValue: Int, falseValue: Int = 0) =
            if (this) trueValue else falseValue
    }

    class Access(
        val db: SQLiteDatabase,
        val context: Context,
    ) {
        fun saveNew(
            acct: String,
            host: String,
            domain: String,
            account: JsonObject,
            token: JsonObject,
            misskeyVersion: Int = 0,
        ): Long {
            try {
                return ContentValues().apply {
                    put(COL_USER, acct)
                    put(COL_HOST, host)
                    put(COL_DOMAIN, domain)
                    put(COL_ACCOUNT, account.toString())
                    put(COL_TOKEN, token.toString())
                    put(COL_MISSKEY_VERSION, misskeyVersion)
                }.let { db.insert(table, null, it) }
            } catch (ex: Throwable) {
                log.e(ex, "SavedAccount.insert failed.")
                errorEx(ex, "SavedAccount.insert failed.")
            }
        }

        fun updateTokenInfo(item: SavedAccount, auth2Result: Auth2Result) {
            item.run {
                if (isInvalidId) error("updateTokenInfo: missing db_id")

                this.tokenJson = auth2Result.tokenJson
                this.loginAccount = auth2Result.tootAccount

                ContentValues().apply {
                    put(COL_TOKEN, auth2Result.tokenJson.toString())
                    put(COL_ACCOUNT, auth2Result.accountJson.toString())
                    put(COL_MISSKEY_VERSION, auth2Result.tootInstance.misskeyVersionMajor)
                }.let { db.update(table, it, "$COL_ID=?", arrayOf(db_id.toString())) }
            }
        }

        /**
         * ユーザ登録の確認手順が完了しているかどうか
         *
         * - マストドン以外だと何もしないはず
         */
        suspend fun checkConfirmed(item: SavedAccount, client: TootApiClient) {
            item.run {
                // 承認待ち状態ではないならチェックしない
                if (loginAccount?.id != EntityId.CONFIRMING) return

                // DBに保存されていないならチェックしない
                if (isInvalidId) return

                // アクセストークンがないならチェックしない
                val accessToken = bearerAccessToken ?: return

                // ユーザ情報を取得してみる。承認済みなら読めるはず
                // 読めなければ例外が出る
                val userJson = client.verifyAccount(
                    accessToken = accessToken,
                    outTokenInfo = null,
                    misskeyVersion = 0, // Mastodon only
                )
                // 読めたらアプリ内の記録を更新する
                TootParser(context, this).account(userJson)?.let { ta ->
                    this.loginAccount = ta
                    db.update(
                        table,
                        ContentValues().apply {
                            put(COL_ACCOUNT, userJson.toString())
                        },
                        "$COL_ID=?",
                        arrayOf(db_id.toString())
                    )
                    checkNotificationImmediateAll(context, onlyEnqueue = true)
                    checkNotificationImmediate(context, db_id)
                }
            }
        }

        fun save(item: SavedAccount) {
            item.run {
                if (isInvalidId) error("saveSetting: missing db_id")

                ContentValues().apply {
                    put(COL_ACCOUNT, accountJson?.toString())
                    put(COL_CONFIRM_BOOST, confirmBoost)
                    put(COL_CONFIRM_FAVOURITE, confirmFavourite)
                    put(COL_CONFIRM_FOLLOW, confirmFollow)
                    put(COL_CONFIRM_FOLLOW_LOCKED, confirmFollowLocked)
                    put(COL_CONFIRM_POST, confirmPost)
                    put(COL_CONFIRM_REACTION, confirmReaction)
                    put(COL_CONFIRM_UNBOOKMARK, confirmUnbookmark)
                    put(COL_CONFIRM_UNBOOST, confirmUnboost)
                    put(COL_CONFIRM_UNFAVOURITE, confirmUnfavourite)
                    put(COL_CONFIRM_UNFOLLOW, confirmUnfollow)
                    put(COL_DEFAULT_SENSITIVE, defaultSensitive)
                    put(COL_DEFAULT_TEXT, defaultText)
                    put(COL_DONT_HIDE_NSFW, dontHideNsfw)
                    put(COL_DONT_SHOW_TIMEOUT, dontShowTimeout)
                    put(COL_EXPAND_CW, expandCw)
                    put(COL_EXTRA_JSON, extraJson.toString())
                    put(COL_IMAGE_MAX_MEGABYTES, imageMaxMegabytes)
                    put(COL_IMAGE_RESIZE, imageResize)
                    put(COL_MAX_TOOT_CHARS, maxTootChars)
                    put(COL_MOVIE_MAX_MEGABYTES, movieMaxMegabytes)
                    put(COL_NOTIFICATION_BOOST, notificationBoost)
                    put(COL_NOTIFICATION_FAVOURITE, notificationFavourite)
                    put(COL_NOTIFICATION_FOLLOW, notificationFollow)
                    put(COL_NOTIFICATION_FOLLOW_REQUEST, notificationFollowRequest)
                    put(COL_NOTIFICATION_MENTION, notificationMention)
                    put(COL_NOTIFICATION_POST, notificationPost)
                    put(COL_NOTIFICATION_REACTION, notificationReaction)
                    put(COL_NOTIFICATION_UPDATE, notificationUpdate)
                    put(COL_NOTIFICATION_VOTE, notificationVote)
                    put(COL_PUSH_POLICY, pushPolicy)
//                    put(COL_SOUND_URI, soundUri)
                    put(COL_TOKEN, tokenJson?.toString())
                    put(COL_VISIBILITY, visibility.id.toString())
                    put(COL_MISSKEY_VERSION, misskeyVersion)
                }.let { db.update(table, it, "$COL_ID=?", arrayOf(db_id.toString())) }
            }
        }

        // onResumeの時に設定を読み直す
        fun reloadSetting(item: SavedAccount, newData: SavedAccount? = null) {
            item.run {

                if (isInvalidId) error("SavedAccount.reloadSetting missing db_id")

                // DBから削除されてるかもしれない
                val b = newData ?: loadAccount(db_id) ?: return
                this.accountJson = b.accountJson
                this.confirmBoost = b.confirmBoost
                this.confirmFavourite = b.confirmFavourite
                this.confirmPost = b.confirmPost
                this.confirmReaction = b.confirmReaction
                this.confirmUnbookmark = b.confirmUnbookmark
                this.confirmUnboost = b.confirmUnboost
                this.confirmUnfavourite = b.confirmUnfavourite
                this.defaultSensitive = b.defaultSensitive
                this.defaultText = b.defaultText
                this.dontHideNsfw = b.dontHideNsfw
                this.dontShowTimeout = b.dontShowTimeout
                this.expandCw = b.expandCw
                this.extraJson = b.extraJson
                this.imageMaxMegabytes = b.imageMaxMegabytes
                this.imageResize = b.imageResize
                this.movieMaxMegabytes = b.movieMaxMegabytes
                this.notificationBoost = b.notificationBoost
                this.notificationFavourite = b.notificationFavourite
                this.notificationFollow = b.notificationFollow
                this.notificationFollowRequest = b.notificationFollowRequest
                this.notificationMention = b.notificationMention
                this.notificationPost = b.notificationPost
                this.notificationReaction = b.notificationReaction
                this.notificationUpdate = b.notificationUpdate
                this.notificationVote = b.notificationVote
                this.pushPolicy = b.pushPolicy
                // this.soundUri = b.soundUri
                this.tokenJson = b.tokenJson
                this.visibility = b.visibility
                this.misskeyVersion = b.misskeyVersion
            }
        }

        fun delete(dbId: Long) {
            try {
                db.deleteById(table, dbId.toString(), COL_ID)
            } catch (ex: Throwable) {
                log.e(ex, "SavedAccount.delete failed.")
                errorEx(ex, "SavedAccount.delete failed.")
            }
        }

//        fun clearRegistrationCache() {
//            ContentValues().apply {
//                put(COL_REGISTER_KEY, REGISTER_KEY_UNREGISTERED)
//                put(COL_REGISTER_TIME, 0L)
//            }.let { db.update(table, it, null, null) }
//        }

        fun loadAccount(dbId: Long): SavedAccount? =
            try {
                db.query(
                    table,
                    null,
                    "$COL_ID=?",
                    arrayOf(dbId.toString()),
                    null,
                    null,
                    null
                )?.use { cursor ->
                    when {
                        cursor.moveToFirst() -> parse(lazyContext, cursor)
                        else -> {
                            log.e("moveToFirst failed. db_id=$dbId")
                            null
                        }
                    }
                }
            } catch (ex: Throwable) {
                log.e(ex, "loadAccount failed.")
                null
            }

        fun loadAccountList() =
            ArrayList<SavedAccount>().also { result ->
                try {
                    db.rawQuery("select * from $table", emptyArray()).use { cursor ->
                        while (cursor.moveToNext()) {
                            parse(lazyContext, cursor)?.let { result.add(it) }
                        }
                    }
                } catch (ex: Throwable) {
                    log.e(ex, "loadAccountList failed.")
                    lazyContext.showToast(
                        true,
                        ex.withCaption("(SubwayTooter) broken in-app database?")
                    )
                }
            }

        fun loadRealAccounts() =
            ArrayList<SavedAccount>().also { result ->
                try {
                    db.rawQuery("select * from $table where $COL_USER not like '?%'", emptyArray())
                        .use { cursor ->
                            while (cursor.moveToNext()) {
                                parse(lazyContext, cursor)?.let { result.add(it) }
                            }
                        }
                } catch (ex: Throwable) {
                    log.e(ex, "loadAccountList failed.")
                    lazyContext.showToast(
                        true,
                        ex.withCaption("(SubwayTooter) broken in-app database?")
                    )
                }
            }

//        fun loadByTag(tag: String): ArrayList<SavedAccount> {
//            val result = ArrayList<SavedAccount>()
//            try {
//                db.query(
//                    table,
//                    null,
//                    "$COL_NOTIFICATION_TAG=?",
//                    arrayOf(tag),
//                    null,
//                    null,
//                    null
//                )
//                    .use { cursor ->
//                        while (cursor.moveToNext()) {
//                            val a = parse(context, cursor)
//                            if (a != null) result.add(a)
//                        }
//                    }
//            } catch (ex: Throwable) {
//                log.e(ex, "loadByTag failed.")
//                errorEx(ex, "SavedAccount.loadByTag failed.")
//            }
//
//            return result
//        }

        /**
         * acctを指定してアカウントを取得する
         */
        fun loadAccountByAcct(fullAcct: Acct) =
            try {
                db.query(
                    table,
                    null,
                    "$COL_USER=?",
                    arrayOf(fullAcct.ascii),
                    null,
                    null,
                    null
                ).use { cursor ->
                    if (cursor.moveToNext()) parse(context, cursor) else null
                }
            } catch (ex: Throwable) {
                log.e(ex, "loadAccountByAcct failed.")
                null
            }

        fun hasRealAccount(): Boolean {
            try {
                db.query(
                    table,
                    null,
                    "$COL_USER NOT LIKE '?@%'",
                    null,
                    null,
                    null,
                    null,
                    "1"
                )
                    .use { cursor ->
                        if (cursor.moveToNext()) {
                            return true
                        }
                    }
            } catch (ex: Throwable) {
                log.e(ex, "hasNonPseudoAccount failed.")
            }

            return false
        }

        fun isSingleAccount(): Boolean =
            try {
                db.rawQuery(
                    "select count(*) from $table where $COL_USER NOT LIKE '?@%' limit 1",
                    emptyArray()
                )?.use {
                    it.moveToNext() && it.getInt(0) == 1
                } ?: false
            } catch (ex: Throwable) {
                log.e(ex, "getCount failed.")
                errorEx(ex, "SavedAccount.getCount failed.")
            }

        //		private fun charAtLower(src : CharSequence, pos : Int) : Char {
        //			val c = src[pos]
        //			return if(c >= 'a' && c <= 'z') c - ('a' - 'A') else c
        //		}
        //
        //		@Suppress("SameParameterValue")
        //		private fun host_match(
        //			a : CharSequence,
        //			a_startArg : Int,
        //			b : CharSequence,
        //			b_startArg : Int
        //		) : Boolean {
        //			var a_start = a_startArg
        //			var b_start = b_startArg
        //
        //			val a_end = a.length
        //			val b_end = b.length
        //
        //			var a_remain = a_end - a_start
        //			val b_remain = b_end - b_start
        //
        //			// 文字数が違う
        //			if(a_remain != b_remain) return false
        //
        //			// 文字数がゼロ
        //			if(a_remain <= 0) return true
        //
        //			// 末尾の文字が違う
        //			if(charAtLower(a, a_end - 1) != charAtLower(b, b_end - 1)) return false
        //
        //			// 先頭からチェック
        //			while(a_remain -- > 0) {
        //				if(charAtLower(a, a_start ++) != charAtLower(b, b_start ++)) return false
        //			}
        //
        //			return true
        //		}

        fun sweepBuggieData() {
            // https://github.com/tateisu/SubwayTooter/issues/107
            // COL_ACCOUNTの内容がおかしければ削除する

            val list = ArrayList<Long>()
            try {
                db.query(
                    table,
                    null,
                    "$COL_ACCOUNT like ?",
                    arrayOf("jp.juggler.subwaytooter.api.entity.TootAccount@%"),
                    null,
                    null,
                    null
                ).use { cursor ->
                    val idxId = cursor.getColumnIndexOrThrow(COL_ID)
                    while (cursor.moveToNext()) {
                        list.add(cursor.getLong(idxId))
                    }
                }
            } catch (ex: Throwable) {
                log.e(ex, "sweepBuggieData failed.")
            }

            list.forEach {
                try {
                    db.delete(table, "$COL_ID=?", arrayOf(it.toString()))
                } catch (ex: Throwable) {
                    log.e(ex, "sweepBuggieData failed.")
                }
            }
        }
    }

    // Notestock検索カラムなど、特定のSNSサーバと紐ついていないアカウントなら真
    val isNA: Boolean
        get() = acct == Acct.UNKNOWN

    // 実在しない疑似アカウントなら真。NAの場合も真を返す
    val isPseudo: Boolean
        get() = username == "?"

    // DB用のIDが無効なら真
    val isInvalidId: Boolean
        get() = db_id <= 0L

    // Mastodonのユーザ作成の承認まち状態ではないなら真
    val isConfirmed: Boolean
        get() = EntityId.CONFIRMING != loginAccount?.id

    // Mastodon用のアクセストークン
    val bearerAccessToken: String?
        get() = tokenJson?.string("access_token")

    // Misskey用のAPIトークン
    val misskeyApiToken: String?
        get() = tokenJson?.string(AuthBase.KEY_API_KEY_MISSKEY)

    override fun hashCode(): Int = acct.hashCode()

    override fun equals(other: Any?): Boolean =
        when (other) {
            is SavedAccount -> acct == other.acct
            else -> false
        }

    // APIリクエスト用のJsonObjectに misskeyApiToken を格納する
    fun putMisskeyApiToken(params: JsonObject = JsonObject()): JsonObject {
        val apiKey = misskeyApiToken
        if (apiKey?.isNotEmpty() == true) params["i"] = apiKey
        return params
    }

    fun getFullAcct(who: TootAccount?) = getFullAcct(who?.acct)

    fun isRemoteUser(who: TootAccount): Boolean = !isLocalUser(who.acct)
    fun isLocalUser(who: TootAccount?): Boolean = isLocalUser(who?.acct)
    private fun isLocalUser(acct: Acct?): Boolean {
        acct ?: return false
        return acct.host == null || acct.host == this.apDomain
    }

    //	fun isRemoteUser(acct : String) : Boolean {
    //		return ! isLocalUser(acct)
    //	}

    fun isMe(who: TootAccount?): Boolean = isMe(who?.acct)
    //	fun isMe(who_acct : String) : Boolean  = isMe(Acct.parse(who_acct))

    fun isMe(acct: Acct?): Boolean {
        acct ?: return false
        if (acct.username != this.acct.username) return false
        return acct.host == null || acct.host == this.acct.host
    }

    // URLが相対指定だった場合にスキーマとホスト名を補う
    fun supplyBaseUrl(url: String?): String? =
        when {
            url.isNullOrEmpty() -> null
            url[0] == '/' -> "https://${apiHost.ascii}$url"
            else -> url
        }

    fun isNicoru(account: TootAccount?) =
        account?.apiHost == Host.FRIENDS_NICO

    fun notificationFlags() = 0 +
            notificationBoost.booleanToInt(1) +
            notificationFavourite.booleanToInt(2) +
            notificationFollow.booleanToInt(4) +
            notificationMention.booleanToInt(8) +
            notificationReaction.booleanToInt(16) +
            notificationVote.booleanToInt(32) +
            notificationFollowRequest.booleanToInt(64) +
            notificationPost.booleanToInt(128) +
            notificationUpdate.booleanToInt(256) +
            notificationStatusReference.booleanToInt(512)

    fun canNotificationShowing(type: String?) = when (type) {

        TootNotification.TYPE_MENTION,
        TootNotification.TYPE_REPLY,
        -> notificationMention

        TootNotification.TYPE_REBLOG,
        TootNotification.TYPE_RENOTE,
        TootNotification.TYPE_QUOTE,
        -> notificationBoost

        TootNotification.TYPE_FAVOURITE -> notificationFavourite

        TootNotification.TYPE_FOLLOW,
        TootNotification.TYPE_UNFOLLOW,
        TootNotification.TYPE_ADMIN_SIGNUP,
        TootNotification.TYPE_ADMIN_REPORT,
        -> notificationFollow

        TootNotification.TYPE_FOLLOW_REQUEST,
        TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
        TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY,
        -> notificationFollowRequest

        TootNotification.TYPE_EMOJI_REACTION_PLEROMA,
        TootNotification.TYPE_EMOJI_REACTION,
        TootNotification.TYPE_REACTION,
        -> notificationReaction

        TootNotification.TYPE_VOTE,
        TootNotification.TYPE_POLL,
        TootNotification.TYPE_POLL_VOTE_MISSKEY,
        -> notificationVote

        TootNotification.TYPE_STATUS -> notificationPost

        TootNotification.TYPE_UPDATE -> notificationUpdate

        TootNotification.TYPE_STATUS_REFERENCE -> notificationStatusReference

        // 未知の通知はオフらない
        else -> true
    }

    fun getResizeConfig() =
        resizeConfigList.find { it.spec == this.imageResize } ?: defaultResizeConfig

    fun getMovieMaxBytes(ti: TootInstance) = 1000000 * max(
        1,
        this.movieMaxMegabytes?.toIntOrNull()
            ?: if (ti.instanceType == InstanceType.Pixelfed) 15 else 40
    )

    fun getImageMaxBytes(ti: TootInstance) = 1000000 * max(
        1,
        this.imageMaxMegabytes?.toIntOrNull()
            ?: if (ti.instanceType == InstanceType.Pixelfed) 15 else 8
    )

    fun getMovieResizeConfig() =
        MovieResizeConfig(
            mode = MovieResizeMode.fromInt(movieTranscodeMode),
            limitBitrate = movieTranscodeBitrate.toLongOrNull()
                ?.takeIf { it >= 100_000L } ?: 2_000_000L,
            limitFrameRate = movieTranscodeFramerate.toIntOrNull()
                ?.takeIf { it >= 1 } ?: 30,
            limitSquarePixels = movieTranscodeSquarePixels.toIntOrNull()
                ?.takeIf { it > 0 } ?: 2304000,
        )

    fun isRequiredPushSubscription() = (notificationFlags() != 0) && notificationPushEnable
    fun isRequiredPullCheck() = (notificationFlags() != 0) && notificationPullEnable
}
