extraction

package site.daydream.colleen

import java.io.InputStream
import java.lang.reflect.Method
import java.lang.reflect.Parameter
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import kotlin.reflect.KFunction
import kotlin.reflect.jvm.javaMethod

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

/**
 * Base exception for parameter extraction errors
 */
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)
}

/**
 * Exception thrown when handler invocation fails
 */
class InvocationError(handler: String, method: String, cause: Throwable) :
    RuntimeException("Failed to invoke $handler.$method", cause)

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

/**
 * Base interface for parameter extractors that wrap extracted values
 */
interface ParamExtractor<T> {
    val value: T
}

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

/** Path parameter extracted from URL segments */
data class Path<T>(override val value: T) : ParamExtractor<T>

/** HTTP header value */
data class Header(override val value: String) : ParamExtractor<String>

/** HTTP cookie value */
data class Cookie(override val value: String) : ParamExtractor<String>

/** Query parameter from URL query string */
data class Query<T>(override val value: T) : ParamExtractor<T>

/** Request body as text */
data class Text<T>(override val value: T) : ParamExtractor<T>

/** JSON request body deserialized to type T */
data class Json<T>(override val value: T) : ParamExtractor<T>

/** Form data parameter */
data class Form<T>(override val value: T) : ParamExtractor<T>

/** Raw request body stream */
data class Stream(override val value: InputStream) : ParamExtractor<InputStream>

/** Uploaded file from multipart request */
data class UploadedFile(override val value: Request.UploadedFile) : ParamExtractor<Request.UploadedFile>

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

/**
 * Specifies the parameter name for extraction.
 * Required for Path, Header, Cookie, and File parameters.
 */
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Param(val value: String = "")

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

/**
 * Converts a Kotlin function reference into a Handler.
 *
 * Supports top-level functions, member functions (bound or unbound), and extension functions.
 * Parameters are automatically extracted from the HTTP context based on their types.
 *
 * Note: Local functions may not work due to Kotlin reflection limitations.
 *
 * @param fn The Kotlin function to wrap
 * @return A Handler that invokes the function with extracted parameters
 * @throws ExtractionError if parameter extraction fails
 * @throws InvocationError if function invocation fails
 */
fun cx(fn: KFunction<*>): Handler {
    val method = fn.javaMethod ?: return createReflectiveHandler(fn)
    return createMethodHandler(method, fn)
}

/**
 * Converts a Java Method into a Handler.
 *
 * Supports both static and instance methods. For instance methods,
 * the instance must be retrieved from the Context via getService().
 *
 * @param method The Java method to wrap
 * @return A Handler that invokes the method with extracted parameters
 * @throws ExtractionError if parameter extraction fails
 * @throws InvocationError if method invocation fails
 */
fun cx(method: Method): Handler {
    return createMethodHandler(method, null)
}

// ===== Handler Creation =====

/**
 * Creates a handler using KFunction.callBy for cases where javaMethod is unavailable
 */
private fun createReflectiveHandler(fn: KFunction<*>): Handler {
    val handlerName = fn.name

    return Handler { ctx ->
        try {
            // Build arguments dynamically using parameter names
            val args = fn.parameters.associateWith { param ->
                when {
                    param.type.classifier == Context::class -> ctx
                    else -> null // Will use default values if available
                }
            }
            fn.callBy(args)
        } catch (e: Exception) {
            throw InvocationError(handlerName, handlerName, e.cause ?: e)
        }
    }
}

/**
 * Creates a handler using Java Method invocation (more efficient, supports full extraction)
 */
private fun createMethodHandler(method: Method, fn: KFunction<*>?): Handler {
    val handlerName = method.declaringClass.simpleName ?: "Handler"
    val isStatic = java.lang.reflect.Modifier.isStatic(method.modifiers)

    // Pre-build extractors for all parameters (closure caching, zero runtime reflection)
    val extractors = method.parameters.map { param ->
        buildExtractor(param, handlerName, method.name)
    }

    return Handler { ctx ->
        try {
            val args = extractors.map { it(ctx) }

            val instance = if (isStatic) {
                null
            } else {
                // For instance methods, retrieve the instance from Context
                ctx.getService(method.declaringClass)
                    ?: throw ExtractionError.MissingParameter(
                        "instance",
                        "Cannot retrieve ${method.declaringClass.simpleName} from context"
                    )
            }

            if (fn != null) {
                // Use KFunction.callBy for proper default parameter support
                val argMap = fn.parameters.zip(args).toMap()
                fn.callBy(argMap)
            } else {
                method.invoke(instance, *args.toTypedArray())
            }
        } catch (e: ExtractionError) {
            throw e
        } catch (e: InvocationError) {
            throw e
        } catch (e: Exception) {
            throw InvocationError(handlerName, method.name, e.cause ?: e)
        }
    }
}

// ===== Extractor Context =====

/**
 * Context information for building extractors, used for error messages
 */
private data class ExtractorContext(
    val handler: String,
    val method: String,
    val paramName: String,
    val paramType: String
) {
    override fun toString() = "$handler.$method($paramType $paramName)"
}

// ===== Parameter Extractor Builder =====

/**
 * Builds a parameter extractor function for a given Java parameter.
 * The extractor is a closure that captures type information and parameter names,
 * eliminating runtime reflection overhead.
 */
private fun buildExtractor(param: Parameter, handler: String, method: String): (Context) -> Any? {
    val rawType = param.type

    // Context is passed directly without extraction
    if (rawType == Context::class.java) {
        return { it }
    }

    val typeInfo = extractTypeInfo(param.parameterizedType)
    val paramName = getParameterName(param)
    val ctx = ExtractorContext(handler, method, paramName, rawType.simpleName)

    return when (rawType) {
        Path::class.java -> buildPathExtractor(ctx, typeInfo)
        Header::class.java -> buildHeaderExtractor(ctx)
        Cookie::class.java -> buildCookieExtractor(ctx)
        Query::class.java -> buildQueryExtractor(ctx, typeInfo)
        Text::class.java -> buildTextExtractor()
        Json::class.java -> buildJsonExtractor(ctx, typeInfo)
        Form::class.java -> buildFormExtractor(ctx, typeInfo)
        Stream::class.java -> buildStreamExtractor(ctx)
        UploadedFile::class.java -> buildFileExtractor(ctx)
        else -> { context ->
            // Try to resolve as a service from the Context
            context.getService(rawType)
        }
    }
}

private fun buildTextExtractor(): (Context) -> Any? {
    // Note: Returns empty string for missing body. 
    // Consider Text<String?> if null distinction is needed.
    return { Text(it.text() ?: "") }
}

// ===== Parameter Name Extraction =====

/**
 * Extracts parameter name from @Param annotation or parameter reflection.
 * Falls back to empty string if name is synthetic (arg0, arg1, etc.)
 */
private fun getParameterName(param: Parameter): String {
    return param.getAnnotation(Param::class.java)?.value?.takeIf { it.isNotEmpty() }
        ?: param.name.takeUnless { it.matches(ARG_PATTERN) }
        ?: ""
}

// ===== Type Information Extraction =====

/**
 * Extracted generic type information from a parameterized type.
 *
 * Limitations:
 * - Only extracts first-level generic arguments (e.g., Query<List<String>>)
 * - Nested generics beyond two levels are not fully supported (e.g., Query<List<Map<String, Foo>>>)
 * - Complex wildcard types are not handled
 *
 * For most REST API use cases, this covers common patterns like:
 * - Query<String>, Query<Int>
 * - Query<List<String>>
 * - Query<Map<String, String>>
 * - Form<CustomClass>
 */
private data class TypeInfo(
    val rawType: Class<*>?,
    val elementType: Class<*>?
)

private fun extractTypeInfo(type: Type): TypeInfo {
    if (type !is ParameterizedType) {
        return TypeInfo(null, null)
    }

    return when (val firstArg = type.actualTypeArguments.firstOrNull()) {
        is Class<*> -> TypeInfo(firstArg, null)
        is ParameterizedType -> {
            val raw = firstArg.rawType as? Class<*>
            val element = if (raw?.isList() == true) {
                firstArg.actualTypeArguments.firstOrNull() as? Class<*>
            } else null
            TypeInfo(raw, element)
        }
        else -> TypeInfo(null, null)
    }
}

// ===== Type Checking Extensions =====

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

// ===== Type-specific Extractors =====

/**
 * Builds extractor for path parameters (e.g., /users/:id)
 * Requires @Param annotation to specify the parameter name
 */
private fun buildPathExtractor(ctx: ExtractorContext, typeInfo: TypeInfo): (Context) -> Any? {
    require(ctx.paramName.isNotEmpty()) {
        "Path parameter requires @Param annotation at $ctx"
    }
    val valueType = typeInfo.rawType
        ?: throw ExtractionError.InvalidType(ctx.paramType, "Path requires generic type at $ctx")

    return { context ->
        val value = context.param(ctx.paramName)
            ?: throw ExtractionError.MissingParameter(ctx.paramName, ctx.paramType)
        Path(value.convert(valueType, ctx))
    }
}

/**
 * Builds extractor for HTTP headers
 * Requires @Param annotation to specify the header name
 */
private fun buildHeaderExtractor(ctx: ExtractorContext): (Context) -> Any? {
    require(ctx.paramName.isNotEmpty()) {
        "Header parameter requires @Param annotation at $ctx"
    }
    return { Header(it.header(ctx.paramName) ?: "") }
}

/**
 * Builds extractor for HTTP cookies
 * Requires @Param annotation to specify the cookie name
 */
private fun buildCookieExtractor(ctx: ExtractorContext): (Context) -> Any? {
    require(ctx.paramName.isNotEmpty()) {
        "Cookie parameter requires @Param annotation at $ctx"
    }
    return { Cookie(it.cookie(ctx.paramName) ?: "") }
}

/**
 * Builds extractor for query parameters
 * Supports: Map, List<T>, simple types, and custom objects
 */
private fun buildQueryExtractor(ctx: ExtractorContext, typeInfo: TypeInfo): (Context) -> Any? {
    val raw = typeInfo.rawType
    val element = typeInfo.elementType

    return { context ->
        val value = when {
            // Query<Map<String, String>> - all query parameters
            raw?.isMap() == true -> context.queries()

            // Query<List<T>> - multiple values for same parameter
            raw?.isList() == true && element != null -> {
                val values = context.request.queryAll(ctx.paramName)
                if (values.isEmpty()) emptyList()
                else values.map { it.convert(element, ctx) }
            }

            // Query<Int>, Query<String>, etc. - single value with type conversion
            raw?.isSimple() == true -> {
                val queryValue = context.query(ctx.paramName)
                queryValue?.convert(raw, ctx)
                    ?: try {
                        getDefaultValue(raw)
                    } catch (e: ExtractionError.InvalidType) {
                        // Cannot provide default - parameter is required
                        throw ExtractionError.MissingParameter(ctx.paramName, ctx.paramType)
                    }
            }

            // Query<CustomClass> - deserialize all queries to object
            raw != null -> context.queriesAs(raw)

            else -> throw ExtractionError.InvalidType(ctx.paramType, "Invalid Query type at $ctx")
        }
        Query(value)
    }
}

/**
 * Builds extractor for JSON request body
 * Deserializes the body to the specified type
 */
private fun buildJsonExtractor(ctx: ExtractorContext, typeInfo: TypeInfo): (Context) -> Any? {
    val valueType = typeInfo.rawType
        ?: throw ExtractionError.InvalidType(ctx.paramType, "Json requires generic type at $ctx")

    return { context ->
        try {
            Json(context.jsonAs(valueType))
        } catch (e: ExtractionError) {
            throw e
        } catch (e: Exception) {
            // Wrap deserialization errors with context
            throw ExtractionError.ConversionFailed(
                "request body",
                "JSON",
                valueType.simpleName,
                e
            )
        }
    }
}

/**
 * Builds extractor for form data parameters
 * Supports: Map, List<T>, simple types, and custom objects
 */
private fun buildFormExtractor(ctx: ExtractorContext, typeInfo: TypeInfo): (Context) -> Any? {
    val raw = typeInfo.rawType
    val element = typeInfo.elementType

    return { context ->
        val value = when {
            // Form<Map<String, String>> - all form parameters
            raw?.isMap() == true -> context.forms()

            // Form<List<T>> - multiple values for same field
            raw?.isList() == true && element != null -> {
                val values = context.request.formAll(ctx.paramName)
                if (values.isEmpty()) emptyList()
                else values.map { it.convert(element, ctx) }
            }

            // Form<Int>, Form<String>, etc. - single value with type conversion
            raw?.isSimple() == true -> {
                val formValue = context.form(ctx.paramName)
                formValue?.convert(raw, ctx)
                    ?: try {
                        getDefaultValue(raw)
                    } catch (e: ExtractionError.InvalidType) {
                        // Cannot provide default - parameter is required
                        throw ExtractionError.MissingParameter(ctx.paramName, ctx.paramType)
                    }
            }

            // Form<CustomClass> - deserialize all form data to object
            raw != null -> {
                try {
                    context.formsAs(raw)
                } catch (e: ExtractionError) {
                    throw e
                } catch (e: Exception) {
                    // Wrap deserialization errors with context
                    throw ExtractionError.ConversionFailed(
                        "form data",
                        "Form",
                        raw.simpleName,
                        e
                    )
                }
            }

            else -> throw ExtractionError.InvalidType(ctx.paramType, "Invalid Form type at $ctx")
        }
        Form(value)
    }
}

/**
 * Builds extractor for raw request body stream
 */
private fun buildStreamExtractor(ctx: ExtractorContext): (Context) -> Any? {
    return { context ->
        Stream(
            context.request.stream
                ?: throw ExtractionError.MissingParameter(
                    "stream",
                    "Request body stream is not available at $ctx"
                )
        )
    }
}

/**
 * Builds extractor for uploaded files
 * Requires @Param annotation to specify the field name
 */
private fun buildFileExtractor(ctx: ExtractorContext): (Context) -> Any? {
    require(ctx.paramName.isNotEmpty()) {
        "File parameter requires @Param annotation at $ctx"
    }
    return { context ->
        val file = context.file(ctx.paramName)
            ?: throw ExtractionError.MissingParameter(ctx.paramName, ctx.paramType)
        UploadedFile(file)
    }
}

// ===== Type Conversion =====

/**
 * Converts a string value to the target type.
 * Supports: String, Int, Long, Double, Float, Boolean
 */
private fun String.convert(targetType: Class<*>, ctx: ExtractorContext): Any {
    return try {
        when (targetType) {
            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 ->
                toBoolean() // Lenient: accepts "true", "TRUE", "1", etc.

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

/**
 * Lenient boolean parsing that accepts common representations:
 * - true: "true", "TRUE", "True", "1", "yes", "YES", "y", "Y"
 * - false: everything else
 */
private fun String.toBoolean(): Boolean {
    return when (this.lowercase()) {
        "true", "1", "yes", "y" -> true
        else -> false
    }
}

/**
 * Gets default value for primitive types.
 * Throws InvalidType for types without a default.
 */
private fun getDefaultValue(clazz: Class<*>): Any = when (clazz) {
    Int::class.java, Integer::class.java -> 0
    Long::class.java, java.lang.Long::class.java -> 0L
    Double::class.java, java.lang.Double::class.java -> 0.0
    Float::class.java, java.lang.Float::class.java -> 0.0f
    Boolean::class.java, java.lang.Boolean::class.java -> false
    String::class.java -> ""
    else -> throw ExtractionError.InvalidType(clazz.simpleName, "No default value available")
}

// ===== Constants =====

/** Set of simple types that can be converted from strings */
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
)

/** Pre-compiled regex for detecting synthetic parameter names */
private val ARG_PATTERN = Regex("arg\\d+")