/*
 * Quasseldroid - Quassel client for Android
 *
 * Copyright (c) 2020 Janne Mareike Koschinski
 * Copyright (c) 2020 The Quassel Project
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3 as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package de.kuschku.quasseldroid.service

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.text.SpannableString
import androidx.core.app.RemoteInput
import androidx.lifecycle.Observer
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import de.kuschku.libquassel.connection.ConnectionState
import de.kuschku.libquassel.connection.SocketAddress
import de.kuschku.libquassel.protocol.*
import de.kuschku.libquassel.quassel.BufferInfo
import de.kuschku.libquassel.quassel.QuasselFeatures
import de.kuschku.libquassel.quassel.syncables.interfaces.IAliasManager
import de.kuschku.libquassel.session.ISession
import de.kuschku.libquassel.session.Session
import de.kuschku.libquassel.session.SessionManager
import de.kuschku.libquassel.session.manager.ConnectionInfo
import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.ERROR
import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.INFO
import de.kuschku.libquassel.util.helper.clampOf
import de.kuschku.libquassel.util.helper.value
import de.kuschku.malheur.CrashHandler
import de.kuschku.quasseldroid.Backend
import de.kuschku.quasseldroid.BuildConfig
import de.kuschku.quasseldroid.Keys
import de.kuschku.quasseldroid.R
import de.kuschku.quasseldroid.defaults.Defaults
import de.kuschku.quasseldroid.persistence.dao.*
import de.kuschku.quasseldroid.persistence.db.AccountDatabase
import de.kuschku.quasseldroid.persistence.db.QuasselDatabase
import de.kuschku.quasseldroid.persistence.util.AccountId
import de.kuschku.quasseldroid.persistence.util.QuasselBacklogStorage
import de.kuschku.quasseldroid.settings.ConnectionSettings
import de.kuschku.quasseldroid.settings.NotificationSettings
import de.kuschku.quasseldroid.settings.Settings
import de.kuschku.quasseldroid.ssl.QuasselHostnameVerifier
import de.kuschku.quasseldroid.ssl.QuasselTrustManager
import de.kuschku.quasseldroid.ssl.custom.QuasselCertificateManager
import de.kuschku.quasseldroid.ssl.custom.QuasselHostnameManager
import de.kuschku.quasseldroid.util.backport.DaggerLifecycleService
import de.kuschku.quasseldroid.util.compatibility.AndroidHandlerService
import de.kuschku.quasseldroid.util.helper.*
import de.kuschku.quasseldroid.util.irc.format.IrcFormatSerializer
import de.kuschku.quasseldroid.util.ui.LocaleHelper
import io.reactivex.subjects.BehaviorSubject
import org.threeten.bp.Instant
import javax.inject.Inject
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.X509TrustManager

class QuasselService : DaggerLifecycleService(),
                       SharedPreferences.OnSharedPreferenceChangeListener {
  @Inject
  lateinit var connectionSettings: ConnectionSettings

  @Inject
  lateinit var notificationSettings: NotificationSettings

  private lateinit var translatedLocale: Context

  override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
    update()
    translatedLocale = LocaleHelper.setLocale(this)
    notificationBackend.updateSettings()
  }

  private fun update() {
    this.notificationSettings = Settings.notification(this)
    val connectionSettings = Settings.connection(this)

    if (this.connectionSettings.showNotification != connectionSettings.showNotification) {
      updateNotificationStatus(this.progress)
    }
    this.connectionSettings = connectionSettings

    val (accountId, reconnect) = this.sharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE) {
      Pair(
        AccountId(getLong(Keys.Status.selectedAccount, -1)),
        getBoolean(Keys.Status.reconnect, false)
      )
    }
    if (this.accountId != accountId || this.reconnect != reconnect) {
      this.accountId = accountId
      this.reconnect = reconnect

      handlerService.backend {
        val account = if (accountId.isValidId() && reconnect) {
          accountDatabase.accounts().findById(accountId)
        } else {
          null
        }

        if (account == null) {
          backendImplementation.disconnect(true)
          stopSelf()
        } else {
          backendImplementation.autoConnect(
            connectionInfo = Backend.ConnectionInfo(
              SocketAddress(account.host, account.port),
              account.user,
              account.pass,
              account.requireSsl,
              true
            )
          )
        }
      }
    } else if (!accountId.isValidId() || !reconnect) {
      handlerService.backend {
        backendImplementation.disconnect(true)
        stopSelf()
      }
    }
  }

  private var accountId: AccountId = AccountId(-1)
    set(value) {
      field = value
      liveAccountId.onNext(value)
    }
  private var liveAccountId = BehaviorSubject.createDefault(AccountId(-1L))
  private var reconnect: Boolean = false

  @Inject
  lateinit var notificationManager: QuasseldroidNotificationManager

  @Inject
  lateinit var notificationBackend: QuasselNotificationBackend

  @Inject
  lateinit var ircFormatSerializer: IrcFormatSerializer

  private var notificationHandle: QuasseldroidNotificationManager.Handle? = null
  private var progress = Triple(ConnectionState.DISCONNECTED, 0, 0)

  private fun updateNotificationStatus(rawProgress: Triple<ConnectionState, Int, Int>) {
    if (connectionSettings.showNotification) {
      val notificationHandle = notificationManager.notificationBackground()
      this.notificationHandle = notificationHandle
      updateNotification(notificationHandle, rawProgress)
      startForeground(notificationHandle.id, notificationHandle.builder.build())
    } else {
      this.notificationHandle = null
      stopForeground(true)
    }
  }

  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    val result = super.onStartCommand(intent, flags, startId)
    handleIntent(intent)
    return result
  }

  private fun handleIntent(intent: Intent?) {
    if (intent == null) return

    if (intent.getBooleanExtra("disconnect", false)) {
      sharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE) {
        editApply {
          putBoolean(Keys.Status.reconnect, false)
        }
      }
    }

    val bufferId = BufferId(intent.getIntExtra("bufferId", -1))

    val inputResults = RemoteInput.getResultsFromIntent(intent)?.getCharSequence("reply_content")
    if (inputResults != null && bufferId.isValidId()) {
      if (inputResults.isNotBlank()) {
        val lines = inputResults.lineSequence().map {
          it.toString() to ircFormatSerializer.toEscapeCodes(SpannableString(it))
        }

        sessionManager.connectedSession.value?.let { session ->
          session.bufferSyncer.bufferInfo(bufferId)?.also { bufferInfo ->
            val output = mutableListOf<IAliasManager.Command>()
            for ((_, formatted) in lines) {
              session.aliasManager.processInput(bufferInfo, formatted, output)
            }
            for (command in output) {
              session.rpcHandler.sendInput(command.buffer, command.message)
            }
          }
          handlerService.backend {
            notificationBackend.showConnectedNotifications()
          }
        }
      }
    } else {
      val clearMessageId = MsgId(intent.getLongExtra("mark_read_message", -1))
      if (bufferId.isValidId() && clearMessageId.isValidId()) {
        sessionManager.connectedSession.value?.bufferSyncer?.requestSetLastSeenMsg(bufferId,
                                                                                   clearMessageId)
        sessionManager.connectedSession.value?.bufferSyncer?.requestMarkBufferAsRead(bufferId)
      }

      val hideMessageId = MsgId(intent.getLongExtra("hide_message", -1))
      if (bufferId.isValidId() && hideMessageId.isValidId()) {
        if (notificationSettings.markReadOnSwipe) {
          sessionManager.connectedSession.value?.bufferSyncer?.requestSetLastSeenMsg(bufferId,
                                                                                     hideMessageId)
          sessionManager.connectedSession.value?.bufferSyncer?.requestMarkBufferAsRead(bufferId)
        } else {
          handlerService.backend {
            database.notifications().markHidden(bufferId, hideMessageId)
          }
        }
      }
    }
  }

  private fun updateNotification(handle: QuasseldroidNotificationManager.Handle,
                                 rawProgress: Triple<ConnectionState, Int, Int>) {
    val (state, progress, max) = rawProgress
    when (state) {
      ConnectionState.DISCONNECTED -> {
        handle.builder.setContentTitle(getString(R.string.label_status_disconnected))
        handle.builder.setProgress(0, 0, true)
      }
      ConnectionState.CONNECTING   -> {
        handle.builder.setContentTitle(getString(R.string.label_status_connecting))
        handle.builder.setProgress(max, progress, true)
      }
      ConnectionState.HANDSHAKE    -> {
        handle.builder.setContentTitle(getString(R.string.label_status_handshake))
        handle.builder.setProgress(max, progress, true)
      }
      ConnectionState.INIT         -> {
        handle.builder.setContentTitle(getString(R.string.label_status_init))
        // Show indeterminate when no progress has been made yet
        handle.builder.setProgress(max, progress, progress == 0 || max == 0)
      }
      ConnectionState.CONNECTED    -> {
        handle.builder.setContentTitle(getString(R.string.label_status_connected))
        handle.builder.setProgress(0, 0, false)
      }
      ConnectionState.CLOSED       -> {
        handle.builder.setContentTitle(getString(R.string.label_status_closed))
        handle.builder.setProgress(0, 0, false)
      }
    }
  }

  private lateinit var sessionManager: SessionManager

  private lateinit var clientData: ClientData

  private lateinit var trustManager: X509TrustManager

  private lateinit var hostnameVerifier: HostnameVerifier

  private lateinit var certificateManager: QuasselCertificateManager

  class BackendImplementation : Backend {
    var service: QuasselService? = null

    override fun updateUserDataAndLogin(user: String, pass: String) {
      service?.apply {
        accountDatabase.accounts().findById(accountId)?.let { old ->
          accountDatabase.accounts().save(old.copy(user = user, pass = pass))
          sessionManager.login(user, pass)
        }
      }
    }

    override fun sessionManager() = service?.sessionManager

    override fun autoConnect(
      ignoreConnectionState: Boolean,
      ignoreSetting: Boolean,
      ignoreErrors: Boolean,
      connectionInfo: Backend.ConnectionInfo?
    ) {
      service?.apply {
        if (connectionInfo != null) {
          sessionManager.autoConnect(
            ignoreConnectionState,
            ignoreSetting,
            ignoreErrors,
            ConnectionInfo(
              clientData = clientData,
              trustManager = trustManager,
              hostnameVerifier = hostnameVerifier,
              address = connectionInfo.address,
              userData = Pair(connectionInfo.username,
                              connectionInfo.password),
              requireSsl = connectionInfo.requireSsl,
              shouldReconnect = connectionInfo.shouldReconnect
            )
          )
        } else {
          sessionManager.autoConnect(ignoreConnectionState, ignoreSetting, ignoreErrors)
        }
      }
    }

    override fun disconnect(forever: Boolean) {
      service?.apply {
        sessionManager.disconnect(forever)
      }
    }

    override fun requestConnectNewNetwork() {
      service?.apply {
        sessionManager.connectedSession.flatMap(ISession::liveNetworkAdded).firstElement().flatMap { id ->
          sessionManager.connectedSession.flatMap(ISession::liveNetworks)
            .map { it[id] }
            .flatMap { network ->
              network.liveInitialized
                .filter { it }
                .map { network }
            }.firstElement()
        }.toLiveData().observe(this, Observer {
          it?.requestConnect()
        })
      }
    }

    override fun setCurrentBuffer(id: BufferId) {
      service?.currentBuffer?.onNext(id)
    }
  }

  private val backendImplementation = BackendImplementation()

  private val handlerService = AndroidHandlerService()

  private val asyncBackend = AsyncBackend(handlerService, backendImplementation)

  @Inject
  lateinit var database: QuasselDatabase

  @Inject
  lateinit var accountDatabase: AccountDatabase

  lateinit var currentBuffer: BehaviorSubject<BufferId>

  private fun disconnectFromCore() {
    getSharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE).editCommit {
      putBoolean(Keys.Status.reconnect, false)
    }
  }

  override fun onCreate() {
    super.onCreate()

    backendImplementation.service = this
    asyncBackend.setDisconnectCallback(::stopSelf)

    translatedLocale = LocaleHelper.setLocale(this)

    certificateManager = QuasselCertificateManager(database.validityWhitelist())
    hostnameVerifier = QuasselHostnameVerifier(QuasselHostnameManager(database.hostnameWhitelist()))
    trustManager = QuasselTrustManager(certificateManager)

    val backlogStorage = QuasselBacklogStorage(database)
    currentBuffer = backlogStorage.currentBuffer
    sessionManager = SessionManager(
      ISession.NULL,
      backlogStorage,
      notificationBackend,
      handlerService,
      ::AndroidHeartBeatRunner,
      CrashHandler::handle
    )

    sessionManager.setDisconnectFromCore(::disconnectFromCore)
    sessionManager.setInitCallback(::initCallback)

    clientData = ClientData(
      identifier = "${resources.getString(R.string.app_name)} ${BuildConfig.FANCY_VERSION_NAME}",
      buildDate = Instant.ofEpochSecond(BuildConfig.GIT_COMMIT_DATE),
      clientFeatures = QuasselFeatures.all(),
      protocolFeatures = Protocol_Features.of(
        Protocol_Feature.Compression,
        Protocol_Feature.TLS
      ),
      supportedProtocols = listOf(Protocol.Datastream)
    )

    sessionManager.connectionProgress.toLiveData().observe(this, Observer {
      if (this.progress.first != it?.first && it?.first == ConnectionState.CONNECTED) {
        handlerService.backend {
          database.message().clearMessages()
        }
      }
      val rawProgress = it ?: Triple(ConnectionState.DISCONNECTED, 0, 0)
      this.progress = rawProgress
      val handle = this.notificationHandle
      if (handle != null) {
        updateNotification(handle, rawProgress)
        notificationManager.notify(handle)
      }
    })

    ReactiveNetwork
      .observeNetworkConnectivity(applicationContext)
      .toLiveData()
      .observe(this, Observer { connectivity ->
        if (!connectionSettings.ignoreNetworkChanges) {
          log(INFO, "QuasselService", "Connectivity changed: $connectivity")
          handlerService.backend {
            log(INFO, "QuasselService", "Reconnect triggered: Network changed")
            sessionManager.autoConnect(
              ignoreConnectionState = true,
              ignoreSetting = true
            )
          }
        }
      })

    sessionManager.state
      .distinctUntilChanged()
      .toLiveData()
      .observe(
        this, Observer {
        handlerService.backend {
          if (it == ConnectionState.HANDSHAKE) {
            backoff = BACKOFF_MIN
          }

          if (it == ConnectionState.CLOSED) {
            scheduleReconnect()
            notificationBackend.showDisconnectedNotifications()
          }
        }
      })

    sharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE) {
      registerOnSharedPreferenceChangeListener(this@QuasselService)
    }
    sharedPreferences {
      registerOnSharedPreferenceChangeListener(this@QuasselService)
    }

    notificationManager.init()

    update()
    updateNotificationStatus(this.progress)
  }

  private var backoff = BACKOFF_MIN
  private var scheduled = false
  private fun scheduleReconnect() {
    if (!scheduled && sessionManager.canAutoReconnect(ignoreSetting = true)) {
      log(INFO, "QuasselService", "Reconnect: Scheduling backoff in ${backoff / 1_000} seconds")
      scheduled = true
      handlerService.backendDelayed(backoff) {
        log(INFO, "QuasselService", "Reconnect: Scheduled backoff happened")
        scheduled = false

        backoff = clampOf(backoff * 2, BACKOFF_MIN, BACKOFF_MAX)
        sessionManager.autoConnect(ignoreSetting = true)
      }
    }
  }

  override fun onDestroy() {
    sharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE) {
      unregisterOnSharedPreferenceChangeListener(this@QuasselService)
    }
    sharedPreferences {
      unregisterOnSharedPreferenceChangeListener(this@QuasselService)
    }

    sessionManager.dispose()
    asyncBackend.setDisconnectCallback(null)
    backendImplementation.service = null

    notificationHandle?.let { notificationManager.remove(it) }
    super.onDestroy()
  }

  override fun onBind(intent: Intent?): QuasselBinder {
    super.onBind(intent)
    return QuasselBinder(asyncBackend)
  }

  private fun initCallback(session: Session) {
    if (session.bufferViewManager.bufferViewConfigs().isEmpty()) {
      session.bufferViewManager.requestCreateBufferView(
        Defaults.bufferViewConfigInitial(translatedLocale).apply {
          for (info in session.bufferSyncer.bufferInfos()) {
            handleBuffer(info, session.bufferSyncer)
          }
        }.toVariantMap()
      )
    }

    // Cleanup deleted buffers from cache

    val buffers = session.bufferSyncer.bufferInfos().map(BufferInfo::bufferId)

    val deletedBuffersMessage = database.message().buffers().toSet() - buffers
    log(INFO, "QuasselService", "Buffers deleted from message storage: $deletedBuffersMessage")
    for (deletedBuffer in deletedBuffersMessage) {
      database.message().clearMessages(deletedBuffer.id)
    }

    val deletedBuffersFiltered = database.filtered().buffers(accountId).toSet() - buffers
    log(INFO, "QuasselService", "Buffers deleted from filtered storage: $deletedBuffersFiltered")
    for (deletedBuffer in deletedBuffersFiltered) {
      database.filtered().clear(accountId, deletedBuffer)
    }

    val deletedBuffersNotifications = database.notifications().buffers().toSet() - buffers
    log(INFO,
        "QuasselService",
        "Buffers deleted from notification storage: $deletedBuffersNotifications")
    for (deletedBuffer in deletedBuffersNotifications) {
      database.notifications().markReadNormal(deletedBuffer)
    }
  }

  companion object {
    // default backoff is 5 seconds
    const val BACKOFF_MIN = 5_000L
    // max is 30 minutes
    const val BACKOFF_MAX = 1_800_000L

    fun launch(
      context: Context,
      disconnect: Boolean? = null,
      markRead: BufferId? = null,
      markReadMessage: MsgId? = null,
      hideMessage: MsgId? = null
    ): ComponentName? = context.startService(
      intent(context, disconnect, markRead, markReadMessage, hideMessage)
    )

    fun intent(
      context: Context,
      disconnect: Boolean? = null,
      bufferId: BufferId? = null,
      markReadMessage: MsgId? = null,
      hideMessage: MsgId? = null
    ) = Intent(context, QuasselService::class.java).apply {
      if (disconnect != null) {
        putExtra("disconnect", disconnect)
      }
      if (bufferId != null) {
        putExtra("bufferId", bufferId.id)
      }
      if (markReadMessage != null) {
        putExtra("mark_read_message", markReadMessage.id)
      }
      if (hideMessage != null) {
        putExtra("hide_message", hideMessage.id)
      }
    }
  }
}