package app.guru.persistent.log

import android.content.Context
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.util.Log
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executors
import java.util.logging.*
import java.util.logging.Formatter
import kotlin.collections.HashMap


object MethodNames {
    const val CREATE = "create_log"
    const val CLEAR = "clear_log"
    const val LOG = "log"
    const val GET_LOG_ROOT_DIR = "get_log_root_dir"
}

object PersistentLoggerConstants {
    object ParamNames {
        const val LOGGER_ID = "logger_id"
        const val LOG_NAME = "log_name"
        const val FILE_SIZE_LIMIT = "file_size"
        const val FILE_COUNT = "file_count"
        const val LEVEL = "level"
        const val FORMATTER = "formatter"
        const val ECHO = "echo"
        const val MESSAGE = "msg"
    }

    object Formatter {
        const val MAIN = 0
        const val RAW = 1
    }
}

class MainFormatter : Formatter() {

    private val dateTimeFormatter = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault())

    override fun format(record: LogRecord?): String {
        if (record == null) return ""
        val formatTime = dateTimeFormatter.format(record.millis)
        val message = formatMessage(record)
        return "$formatTime ${record.level.name} $message\n"
    }
}

class RawFormatter : Formatter() {

    override fun format(record: LogRecord?): String {
        if (record == null) return ""
        return "${formatMessage(record)}\n"
    }
}

class PersistentLog(private val context: Context, messenger: BinaryMessenger) {

    companion object {
        const val channelName = "app.guru.persistent.LOG"
        private const val LOGS_DIRECTORY = "guru/logs"
        private val defaultLevel = object : Level("default", 2627, "app.guru.log") {}
    }

    private val channel: MethodChannel =
        MethodChannel(messenger, channelName)

    init {
        channel.setMethodCallHandler { call, result ->
            dispatch(call, result)
        }
    }

    private val handler = Handler(Looper.getMainLooper())

    private val deliverExecutor = Executors.newSingleThreadExecutor()

    private val resolvedLogsDirectory by lazy {
        File(
            if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() && !Environment.isExternalStorageRemovable()) {
                context.getExternalFilesDir(null)
            } else {
                context.filesDir
            }, LOGS_DIRECTORY
        ).also { it.mkdirs() }
    }

    private val loggers = HashMap<String, Logger>()

    fun dispose() {
        channel.setMethodCallHandler(null)
    }

    private fun getLogger(name: String): Logger? {
        return loggers[name]
    }

    private fun createLogger(call: MethodCall, result: MethodChannel.Result) {
        val logName =
            call.argument<String>(PersistentLoggerConstants.ParamNames.LOG_NAME)
        val fileSizeLimit =
            call.argument<Int>(PersistentLoggerConstants.ParamNames.FILE_SIZE_LIMIT)
        val fileCount =
            call.argument<Int>(PersistentLoggerConstants.ParamNames.FILE_COUNT)
        val echo =
            call.argument<Boolean>(PersistentLoggerConstants.ParamNames.ECHO) ?: false
        Log.d("PLC", "createLogger: $logName $fileSizeLimit $fileCount ")

        if (logName == null || fileSizeLimit == null || fileCount == null) {
            result.error("INVALID_ARGUMENTS", "Invalid arguments", null)
            return
        }
        val filePattern = resolvedLogsDirectory.path + "/" + logName
        val l = getLogger(logName)
        if (l != null) {
            result.error("ALREADY_EXISTS", "Logger already exists", null)
            return
        }
        val formatter =
            when (call.argument<Int>(PersistentLoggerConstants.ParamNames.FORMATTER)) {
                PersistentLoggerConstants.Formatter.RAW -> RawFormatter()
                else -> MainFormatter()
            }
        val logger = Logger.getLogger(logName).also {
            kotlin.runCatching {
                FileHandler(filePattern, fileSizeLimit, fileCount, true)
            }.getOrNull()?.let { fileHandler ->
                fileHandler.formatter = formatter
                it.useParentHandlers = echo
                it.addHandler(fileHandler)
            }
        }
        loggers[logName] = logger
        result.success(true)
    }

    private fun clearLogger(call: MethodCall, result: MethodChannel.Result) {
        val logName =
            call.argument<String>(PersistentLoggerConstants.ParamNames.LOG_NAME)
        if (logName == null) {
            result.error("INVALID_ARGUMENTS", "Invalid arguments", null)
            return
        }
        val logDir = resolvedLogsDirectory.path
        val file = File(logDir)
        file.listFiles { _, name ->
            name.startsWith(logName)
        }?.forEach {
            it.delete()
        }
        loggers.remove(logName)
        result.success(true)
    }

    private fun log(call: MethodCall, result: MethodChannel.Result) {
        val logName =
            call.argument<String>(PersistentLoggerConstants.ParamNames.LOG_NAME)
        val msg = call.argument<String>(PersistentLoggerConstants.ParamNames.MESSAGE)
        if (logName == null || msg == null) {
            result.error("INVALID_ARGUMENTS", "Invalid arguments", null)
            return
        }

        getLogger(logName)?.run {
            deliverExecutor.execute {
                Log.d("PLC", "[log]: $logName $msg")
                log(defaultLevel, msg)
            }
        }
        Log.d("PLC", "log COMPLETE: $logName $msg")
        result.success(true)
    }

    private fun getLogRootDir(call: MethodCall, result: MethodChannel.Result) {
        result.success(resolvedLogsDirectory.path)
    }

    private fun dispatch(call: MethodCall, result: MethodChannel.Result) {
        Log.d("PLC", "dispatch: ${call.method}")
        when (call.method) {
            MethodNames.CREATE -> {
                handler.post {
                    createLogger(call, result)
                }
            }

            MethodNames.CLEAR -> {
                handler.post {
                    clearLogger(call, result)
                }
            }

            MethodNames.LOG -> {
                handler.post {
                    log(call, result)
                }
            }

            MethodNames.GET_LOG_ROOT_DIR -> {
                handler.post {
                    getLogRootDir(call, result)
                }
            }
            else -> {
                handler.post {
                    result.error("not impl!!!", "${call.method} not impl!", "")
                }
            }
        }

    }

}