extractor

package site.daydream.colleen

import java.io.InputStream
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation

// ===== Exception Definitions =====

sealed class ExtractionError(message: String, cause: Throwable? = null) : RuntimeException(message, cause) {
    class MissingParameter(name: String, type: String) :
        ExtractionError("Required parameter '$name' of type $type not found")

    class InvalidType(type: String, reason: String) :
        ExtractionError("Invalid type $type: $reason")

    class ConversionFailed(value: String, from: String, to: String, cause: Throwable? = null) :
        ExtractionError("Cannot convert '$value' from $from to $to", cause)
}

// ===== Parameter Extractor Interface =====

interface ParamExtractor<T> {
    val value: T
}

// ===== Parameter Wrapper Types =====

data class Path<T>(override val value: T) : ParamExtractor<T>
data class Header(override val value: String) : ParamExtractor<String>
data class Cookie(override val value: String) : ParamExtractor<String>
data class Query<T>(override val value: T) : ParamExtractor<T>
data class Text(override val value: String) : ParamExtractor<String>
data class Json<T>(override val value: T) : ParamExtractor<T>
data class Form<T>(override val value: T) : ParamExtractor<T>
data class Stream(override val value: InputStream) : ParamExtractor<InputStream>
data class UploadedFile(override val value: Request.UploadedFile) : ParamExtractor<Request.UploadedFile>

// ===== Annotations =====

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Param(val value: String = "")

// ===== cx Function Implementation =====

fun cx(fn: KFunction<*>): Handler {
    val valueParams = fn.parameters.filter { it.kind == KParameter.Kind.VALUE }
    val extractors = valueParams.map { param -> buildExtractor(param, fn.name) }

    return Handler { ctx ->
        val args = extractors.map { it(ctx) }
        fn.callBy(valueParams.zip(args).toMap())
    }
}

// ===== Core Extractor Builder =====

private fun buildExtractor(param: KParameter, handlerName: String): (Context) -> Any? {
    val kType = param.type
    val wrapperClass = kType.classifier as? kotlin.reflect.KClass<*>
        ?: throw ExtractionError.InvalidType(kType.toString(), "Cannot resolve classifier")

    // Context 直接传递
    if (wrapperClass == Context::class) {
        return { it }
    }

    val paramName = param.findAnnotation<Param>()?.value?.takeIf { it.isNotEmpty() }
        ?: param.name
        ?: ""

    return when (wrapperClass) {
        Path::class -> buildPathExtractor(paramName, kType, handlerName)
        Header::class -> buildHeaderExtractor(paramName)
        Cookie::class -> buildCookieExtractor(paramName)
        Query::class -> buildQueryExtractor(paramName, kType, handlerName)
        Form::class -> buildFormExtractor(paramName, kType, handlerName)
        Json::class -> buildJsonExtractor(kType, handlerName)
        Stream::class -> buildStreamExtractor()
        UploadedFile::class -> buildFileExtractor(paramName)
        Text::class -> { ctx -> Text(ctx.text() ?: "") }
        else -> { ctx -> ctx.getService(wrapperClass.java) }
    }
}

// ===== 泛型类型解析 =====

/**
 * 解析 Wrapper<T> 中的 T 类型
 * 例如:Query<Int> -> GenericType(Int, nullable=false)
 */
private fun KType.unwrapGeneric(): GenericType {
    val typeArg = arguments.firstOrNull()?.type
        ?: throw ExtractionError.InvalidType(toString(), "Missing generic type parameter")

    val javaClass = (typeArg.classifier as? kotlin.reflect.KClass<*>)?.java
        ?: throw ExtractionError.InvalidType(typeArg.toString(), "Cannot resolve class")

    return GenericType(javaClass, typeArg.isMarkedNullable)
}

/**
 * 解析 Wrapper<List<T>> 中的元素类型 T
 * 例如:Query<List<Int>> -> Int
 */
private fun KType.unwrapListElement(): Class<*> {
    val listType = arguments.firstOrNull()?.type
        ?: throw ExtractionError.InvalidType(toString(), "Missing generic type")

    val elementType = listType.arguments.firstOrNull()?.type
        ?: throw ExtractionError.InvalidType(listType.toString(), "List missing element type")

    return (elementType.classifier as? kotlin.reflect.KClass<*>)?.java
        ?: throw ExtractionError.InvalidType(elementType.toString(), "Cannot resolve element class")
}

private data class GenericType(val javaClass: Class<*>, val isNullable: Boolean)

// ===== 类型判断扩展 =====

private fun Class<*>.isSimple() = this in SIMPLE_TYPES
private fun Class<*>.isMap() = Map::class.java.isAssignableFrom(this)
private fun Class<*>.isList() = List::class.java.isAssignableFrom(this)

private val SIMPLE_TYPES = setOf(
    String::class.java,
    Int::class.java, Integer::class.java,
    Long::class.java, java.lang.Long::class.java,
    Double::class.java, java.lang.Double::class.java,
    Float::class.java, java.lang.Float::class.java,
    Boolean::class.java, java.lang.Boolean::class.java
)

// ===== 各类型提取器 =====

private fun buildPathExtractor(name: String, kType: KType, handler: String): (Context) -> Path<*> {
    require(name.isNotEmpty()) { "Path parameter requires @Param annotation in $handler" }

    val generic = kType.unwrapGeneric()
    require(!generic.isNullable) { "Path parameter cannot be nullable in $handler" }

    return { ctx ->
        val value = ctx.param(name)
            ?: throw ExtractionError.MissingParameter(name, "Path")
        Path(value.convertTo(generic.javaClass, name))
    }
}

private fun buildHeaderExtractor(name: String): (Context) -> Header {
    require(name.isNotEmpty()) { "Header parameter requires @Param annotation" }
    return { ctx -> Header(ctx.header(name) ?: "") }
}

private fun buildCookieExtractor(name: String): (Context) -> Cookie {
    require(name.isNotEmpty()) { "Cookie parameter requires @Param annotation" }
    return { ctx -> Cookie(ctx.cookie(name) ?: "") }
}

private fun buildQueryExtractor(name: String, kType: KType, handler: String): (Context) -> Query<*> {
    val generic = kType.unwrapGeneric()
    val targetClass = generic.javaClass

    // Query<Map<String, List<String>>>
    if (targetClass.isMap()) {
        return { ctx -> Query(ctx.queries()) }
    }

    // Query<List<T>>
    if (targetClass.isList()) {
        val elementClass = kType.unwrapListElement()
        return { ctx ->
            val values = ctx.request.queryAll(name)
            Query(values.map { it.convertTo(elementClass, name) })
        }
    }

    // Query<Int> 或 Query<Int?>
    if (targetClass.isSimple()) {
        return { ctx ->
            val value = ctx.query(name)
            when {
                value != null -> Query(value.convertTo(targetClass, name))
                generic.isNullable -> Query(null)
                else -> throw ExtractionError.MissingParameter(name, "Query")
            }
        }
    }

    // Query<CustomDto>
    return { ctx ->
        try {
            Query(ctx.queriesAs(targetClass)!!)
        } catch (e: Exception) {
            throw ExtractionError.ConversionFailed("query data", "Query", targetClass.simpleName, e)
        }
    }
}

private fun buildFormExtractor(name: String, kType: KType, handler: String): (Context) -> Form<*> {
    val generic = kType.unwrapGeneric()
    val targetClass = generic.javaClass

    // Form<Map<String, List<String>>>
    if (targetClass.isMap()) {
        return { ctx -> Form(ctx.forms()) }
    }

    // Form<List<T>>
    if (targetClass.isList()) {
        val elementClass = kType.unwrapListElement()
        return { ctx ->
            val values = ctx.request.formAll(name)
            Form(values.map { it.convertTo(elementClass, name) })
        }
    }

    // Form<Int> 或 Form<Int?>
    if (targetClass.isSimple()) {
        return { ctx ->
            val value = ctx.form(name)
            when {
                value != null -> Form(value.convertTo(targetClass, name))
                generic.isNullable -> Form(null)
                else -> throw ExtractionError.MissingParameter(name, "Form")
            }
        }
    }

    // Form<CustomDto>
    return { ctx ->
        try {
            Form(ctx.formsAs(targetClass)!!)
        } catch (e: Exception) {
            throw ExtractionError.ConversionFailed("form data", "Form", targetClass.simpleName, e)
        }
    }
}

private fun buildJsonExtractor(kType: KType, handler: String): (Context) -> Json<*> {
    val targetClass = kType.unwrapGeneric().javaClass

    return { ctx ->
        try {
            Json(ctx.jsonAs(targetClass))
        } catch (e: Exception) {
            throw ExtractionError.ConversionFailed("request body", "JSON", targetClass.simpleName, e)
        }
    }
}

private fun buildStreamExtractor(): (Context) -> Stream {
    return { ctx ->
        Stream(ctx.request.stream ?: throw ExtractionError.MissingParameter("stream", "InputStream"))
    }
}

private fun buildFileExtractor(name: String): (Context) -> UploadedFile {
    // require(name.isNotEmpty()) { "File parameter requires @Param annotation" }
    return { ctx ->
        val file = ctx.file(name.ifEmpty { "file" })
            ?: throw ExtractionError.MissingParameter(name, "UploadedFile")
        UploadedFile(file)
    }
}

// ===== 类型转换 =====

private fun String.convertTo(targetClass: Class<*>, paramName: String): Any {
    return try {
        when (targetClass) {
            String::class.java -> this
            Int::class.java, Integer::class.java ->
                toIntOrNull() ?: throw ExtractionError.ConversionFailed(this, "String", "Int")

            Long::class.java, java.lang.Long::class.java ->
                toLongOrNull() ?: throw ExtractionError.ConversionFailed(this, "String", "Long")

            Double::class.java, java.lang.Double::class.java ->
                toDoubleOrNull() ?: throw ExtractionError.ConversionFailed(this, "String", "Double")

            Float::class.java, java.lang.Float::class.java ->
                toFloatOrNull() ?: throw ExtractionError.ConversionFailed(this, "String", "Float")

            Boolean::class.java, java.lang.Boolean::class.java ->
                toLenientBoolean()

            else ->
                throw ExtractionError.InvalidType(targetClass.simpleName, "Unsupported conversion type")
        }
    } catch (e: ExtractionError) {
        throw e
    } catch (e: Exception) {
        throw ExtractionError.ConversionFailed(this, "String", targetClass.simpleName, e)
    }
}

private fun String.toLenientBoolean(): Boolean {
    return lowercase() in setOf("true", "1", "yes", "y")
}