package com.correx.apps.cli.commands

import com.correx.apps.cli.CorrexCli
import com.correx.apps.cli.DEFAULT_PORT
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.multiple
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import java.io.File
import java.net.URLEncoder
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put

private val taskJson = Json { ignoreUnknownKeys = true }

private const val TASK_ID_COLUMN_WIDTH = 14
private const val TASK_STATUS_COLUMN_WIDTH = 12

/** Subset of the server's TaskResponse the CLI renders. Unknown fields are ignored. */
@Serializable
data class TaskRow(
    val id: String,
    val key: String? = null,
    val status: String = "",
    val title: String? = null,
    val goal: String? = null,
    val acceptanceCriteria: List<String> = emptyList(),
    val affectedPaths: List<String> = emptyList(),
    val claimant: String? = null,
    val links: List<TaskLinkRow> = emptyList(),
    val notes: List<TaskNoteRow> = emptyList(),
)

@Serializable
data class TaskLinkRow(val targetId: String, val type: String, val targetKind: String)

@Serializable
data class TaskNoteRow(val author: String, val body: String)

/** One line per task: id, status, title, and claimant. */
fun renderTaskList(tasks: List<TaskRow>): String {
    if (tasks.isEmpty()) return "no tasks"
    return tasks.joinToString("\n") { t ->
        val claim = t.claimant?.let { "  @$it" }.orEmpty()
        val id = t.id.padEnd(TASK_ID_COLUMN_WIDTH)
        val status = t.status.padEnd(TASK_STATUS_COLUMN_WIDTH)
        "$id $status ${t.title.orEmpty()}$claim".trimEnd()
    }
}

/** Full single-task view: goal, acceptance criteria, affected paths, links, notes. */
fun renderTaskDetail(t: TaskRow): String = buildString {
    appendLine("task ${t.id} [${t.status}] ${t.title.orEmpty()}".trimEnd())
    t.claimant?.let { appendLine("claimant: $it") }
    t.goal?.let { appendLine("goal: $it") }
    if (t.acceptanceCriteria.isNotEmpty()) {
        appendLine("acceptance criteria:")
        t.acceptanceCriteria.forEach { appendLine("  - $it") }
    }
    if (t.affectedPaths.isNotEmpty()) appendLine("affected paths: ${t.affectedPaths.joinToString(", ")}")
    if (t.links.isNotEmpty()) {
        appendLine("links:")
        t.links.forEach { appendLine("  - ${it.targetId} (${it.type} -> ${it.targetKind})") }
    }
    if (t.notes.isNotEmpty()) {
        appendLine("notes:")
        t.notes.forEach { appendLine("  - [${it.author}] ${it.body}") }
    }
}.trimEnd()

class TaskCommand : CliktCommand(name = "task") {
    override fun run() = Unit

    init {
        subcommands(
            TaskListCommand(),
            TaskReadyCommand(),
            TaskSearchCommand(),
            TaskShowCommand(),
            TaskBlockersCommand(),
            TaskHistoryCommand(),
            TaskCreateCommand(),
            TaskClaimCommand(),
            TaskCompleteCommand(),
            TaskExportCommand(),
        )
    }
}

/** Shared host/port options + HTTP plumbing for the task subcommands. */
abstract class TaskHttpCommand(name: String) : CliktCommand(name = name) {
    protected val host by option("--host").default("localhost")
    protected val port by option("--port").default("$DEFAULT_PORT")

    protected fun baseUrl(): String = "http://$host:${port.toIntOrNull() ?: DEFAULT_PORT}"

    protected fun jsonOut(): Boolean = (currentContext.findRoot().command as? CorrexCli)?.json ?: false

    protected fun http(block: suspend (HttpClient) -> Unit) = runBlocking {
        val client = HttpClient(CIO) { install(ContentNegotiation) { json(taskJson) } }
        runCatching { block(client) }.getOrElse { System.err.println("Error: ${it.message}") }
        client.close()
    }
}

private fun enc(value: String): String = URLEncoder.encode(value, "UTF-8")

class TaskListCommand : TaskHttpCommand("list") {
    private val project by option("--project", help = "Limit to a project")
    private val status by option("--status", help = "Filter by status (TODO, IN_PROGRESS, …)")

    override fun run() = http { client ->
        val params = buildList {
            project?.let { add("project=${enc(it)}") }
            status?.let { add("status=${enc(it)}") }
        }
        val query = if (params.isEmpty()) "" else "?${params.joinToString("&")}"
        val raw = client.get("${baseUrl()}/tasks$query").bodyAsText()
        if (jsonOut()) println(raw) else println(renderTaskList(taskJson.decodeFromString(raw)))
    }
}

class TaskReadyCommand : TaskHttpCommand("ready") {
    private val project by option("--project", help = "Limit to a project")

    override fun run() = http { client ->
        val query = project?.let { "&project=${enc(it)}" }.orEmpty()
        val raw = client.get("${baseUrl()}/tasks?ready=true$query").bodyAsText()
        if (jsonOut()) println(raw) else println(renderTaskList(taskJson.decodeFromString(raw)))
    }
}

class TaskBlockersCommand : TaskHttpCommand("blockers") {
    private val id by argument("TASK_ID")

    override fun run() = http { client ->
        val resp = client.get("${baseUrl()}/tasks/$id/blockers")
        if (resp.status == HttpStatusCode.NotFound) {
            System.err.println("No such task: $id")
        } else {
            val raw = resp.bodyAsText()
            if (jsonOut()) println(raw) else println(renderTaskList(taskJson.decodeFromString(raw)))
        }
    }
}

class TaskSearchCommand : TaskHttpCommand("search") {
    private val query by argument("QUERY", help = "Search text (quote multi-word queries)")
    private val project by option("--project", help = "Limit to a project")

    override fun run() = http { client ->
        val params = buildList {
            add("q=${enc(query)}")
            project?.let { add("project=${enc(it)}") }
        }
        val raw = client.get("${baseUrl()}/tasks?${params.joinToString("&")}").bodyAsText()
        if (jsonOut()) println(raw) else println(renderTaskList(taskJson.decodeFromString(raw)))
    }
}

class TaskShowCommand : TaskHttpCommand("show") {
    private val id by argument("TASK_ID")

    override fun run() = http { client ->
        val resp = client.get("${baseUrl()}/tasks/$id")
        if (resp.status == HttpStatusCode.NotFound) {
            System.err.println("No such task: $id")
        } else {
            val raw = resp.bodyAsText()
            if (jsonOut()) println(raw) else println(renderTaskDetail(taskJson.decodeFromString(raw)))
        }
    }
}

class TaskHistoryCommand : TaskHttpCommand("history") {
    private val id by argument("TASK_ID")

    override fun run() = http { client ->
        val resp = client.get("${baseUrl()}/tasks/$id/history")
        if (resp.status == HttpStatusCode.NotFound) {
            System.err.println("No such task: $id")
        } else {
            print(resp.bodyAsText())
        }
    }
}

class TaskCreateCommand : TaskHttpCommand("create") {
    private val project by option("--project", help = "Project key, e.g. 'auth'").required()
    private val title by option("--title").required()
    private val goal by option("--goal").required()
    private val criteria by option("--criteria", help = "Acceptance criterion (repeatable)").multiple()
    private val paths by option("--path", help = "Affected path/glob (repeatable)").multiple()
    private val force by option("--force", help = "Create even if a same-title task exists").flag()

    override fun run() = http { client ->
        val payload = buildJsonObject {
            put("project", project)
            put("title", title)
            put("goal", goal)
            if (criteria.isNotEmpty()) put("acceptanceCriteria", JsonArray(criteria.map { JsonPrimitive(it) }))
            if (paths.isNotEmpty()) put("affectedPaths", JsonArray(paths.map { JsonPrimitive(it) }))
            if (force) put("force", true)
        }
        val resp = client.post("${baseUrl()}/tasks") {
            contentType(ContentType.Application.Json)
            setBody(payload)
        }
        val raw = resp.bodyAsText()
        when {
            resp.status == HttpStatusCode.Conflict -> {
                val dupes = renderTaskList(taskJson.decodeFromString(raw))
                System.err.println("Possible duplicate(s) — pass --force to create anyway:\n$dupes")
            }
            jsonOut() -> println(raw)
            else -> println("created ${taskJson.decodeFromString<TaskRow>(raw).id}")
        }
    }
}

class TaskClaimCommand : TaskHttpCommand("claim") {
    private val id by argument("TASK_ID")
    private val by by option("--by", help = "Claimant").required()

    override fun run() = http { client ->
        val resp = client.post("${baseUrl()}/tasks/$id/claim") {
            contentType(ContentType.Application.Json)
            setBody(buildJsonObject { put("claimant", by) })
        }
        if (resp.status == HttpStatusCode.NotFound) System.err.println("No such task: $id") else println("claimed $id")
    }
}

class TaskCompleteCommand : TaskHttpCommand("complete") {
    private val id by argument("TASK_ID")

    override fun run() = http { client ->
        val resp = client.post("${baseUrl()}/tasks/$id/complete")
        if (resp.status == HttpStatusCode.NotFound) {
            System.err.println("No such task: $id")
        } else {
            println("completed $id")
        }
    }
}

class TaskExportCommand : TaskHttpCommand("export") {
    private val project by option("--project", help = "Limit to a project")
    private val out by option("--out", help = "Write to this file instead of stdout")

    override fun run() = http { client ->
        val query = project?.let { "?project=${enc(it)}" }.orEmpty()
        val markdown = client.get("${baseUrl()}/tasks/export$query").bodyAsText()
        val target = out
        if (target != null) {
            File(target).writeText(markdown)
            println("wrote $target")
        } else {
            print(markdown)
        }
    }
}
