package com.correx.apps.server.routes

import com.correx.apps.server.ServerModule
import com.correx.core.events.types.ProjectId
import com.correx.core.tasks.Commit
import com.correx.core.tasks.GitCommitReader
import com.correx.core.tasks.GitTaskSync
import com.correx.core.events.types.TaskId
import com.correx.core.events.types.TaskLinkType
import com.correx.core.events.types.TaskNoteAuthor
import com.correx.core.events.types.TaskTargetKind
import com.correx.core.tasks.Task
import com.correx.core.tasks.TaskContextAssembler
import com.correx.core.tasks.TaskGraph
import com.correx.core.tasks.TaskHistory
import com.correx.core.tasks.TaskMarkdown
import com.correx.core.tasks.TaskSearch
import com.correx.core.tasks.TaskService
import com.correx.core.tasks.TaskStatus
import com.correx.core.tasks.TaskTargetKinds
import com.correx.core.utils.TypeId
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.RoutingContext
import io.ktor.server.routing.delete
import io.ktor.server.routing.get
import io.ktor.server.routing.patch
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import kotlinx.serialization.Serializable

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

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

@Serializable
data class TaskResponse(
    val id: String,
    val key: String?,
    val status: String,
    val title: String?,
    val goal: String?,
    val acceptanceCriteria: List<String>,
    val affectedPaths: List<String>,
    val claimant: String?,
    val links: List<TaskLinkDto>,
    val notes: List<TaskNoteDto>,
    val createdAt: String?,
    val updatedAt: String?,
    // Dependency-graph status for the board: ready = TODO with every dependency satisfied (workable
    // now); blockedBy = the ids of unfinished tasks this one waits on (its unmet DEPENDS_ON/BLOCKS).
    // Default to "not blocked / not ready" for the endpoints that don't resolve the graph.
    val ready: Boolean = false,
    val blockedBy: List<String> = emptyList(),
)

@Serializable
data class CreateTaskRequest(
    val project: String,
    val title: String,
    val goal: String,
    val acceptanceCriteria: List<String> = emptyList(),
    val affectedPaths: List<String> = emptyList(),
    // Skip the duplicate-title guard and create anyway.
    val force: Boolean = false,
)

// Null fields are left untouched; a present list (even empty) replaces the stored one.
@Serializable
data class EditTaskRequest(
    val title: String? = null,
    val goal: String? = null,
    val acceptanceCriteria: List<String>? = null,
    val affectedPaths: List<String>? = null,
)

@Serializable
data class ClaimRequest(val claimant: String)

@Serializable
data class BlockRequest(val reason: String)

@Serializable
data class CancelRequest(val reason: String? = null)

@Serializable
data class LinkRequest(val targetId: String, val type: String = "RELATES_TO", val targetKind: String? = null)

@Serializable
data class NoteRequest(val body: String, val author: String? = null)

@Serializable
data class CommitDto(val sha: String = "", val message: String)

// When commits are supplied they are used verbatim; otherwise the server reads the last `limit`
// commits from the repo (when a GitCommitReader is wired).
@Serializable
data class SyncGitRequest(val commits: List<CommitDto> = emptyList(), val limit: Int = DEFAULT_SYNC_LIMIT)

private const val DEFAULT_SYNC_LIMIT = 20

/**
 * Full REST surface for tasks. The write path mirrors the [TaskService] / `task_*` tools; both
 * append to the event log, which stays the single source of truth (nothing is stored separately).
 * A task's project is encoded in its id, so item routes need only the `{id}`. The list route takes
 * optional `project` (cross-project board without it), `status`, and `q` (ranked text search);
 * create requires `project` in the body. [GET /tasks/{id}/context] serves the agent bundle.
 */
fun Route.taskRoutes(module: ServerModule) {
    val service = TaskService(module.eventStore)
    val assembler = TaskContextAssembler(
        service,
        module.taskKnowledgeRetriever,
        module.taskDocumentResolver,
        module.taskArtifactResolver,
        module.taskSessionResolver,
    )
    route("/tasks") {
        taskCollectionRoutes(service)
        taskSyncRoute(service, module.gitCommitReader)
        route("/{id}") {
            taskCrudRoutes(service, assembler)
            taskTransitionRoutes(service)
            taskGraphRoutes(service)
        }
    }
}

private fun Route.taskCollectionRoutes(service: TaskService) {
    get {
        // ?ready=true short-circuits to the dependency-aware workable set (TODO, no unmet blockers).
        if (call.request.queryParameters["ready"]?.toBoolean() == true) {
            val readyProject = call.request.queryParameters["project"]
            val workable = service.ready(readyProject?.let { ProjectId(it) })
            return@get call.respond(workable.map { it.toResponse().copy(ready = true) })
        }
        val statusRaw = call.request.queryParameters["status"]
        val statusFilter = statusRaw?.let {
            parseEnum<TaskStatus>(it) ?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid status: $it")
        }
        // project is optional: scoped to one project when given, else the whole cross-project board.
        val project = call.request.queryParameters["project"]
        val base = (if (project != null) service.list(ProjectId(project)) else service.listAll())
            .filter { statusFilter == null || it.state.status == statusFilter }
        // q ranks by relevance; without it the board is most-recently-touched first.
        val q = call.request.queryParameters["q"]
        val ordered = if (q.isNullOrBlank()) {
            base.sortedByDescending { it.state.updatedAt }
        } else {
            TaskSearch.search(base, q)
        }
        // Resolve ready/blockedBy against each task's FULL project board (not the filtered list — a
        // status filter must not hide a finished blocker). One list() per distinct project, cached.
        val boards = HashMap<ProjectId, List<Task>>()
        fun enrich(t: Task): TaskResponse {
            val pid = t.state.projectId ?: return t.toResponse()
            val board = boards.getOrPut(pid) { service.list(pid) }
            return t.toResponse().copy(
                ready = TaskGraph.isReady(t, board),
                blockedBy = TaskGraph.unmetBlockers(t, board).map { it.taskId.value },
            )
        }
        call.respond(ordered.map(::enrich))
    }
    post {
        val body = call.receive<CreateTaskRequest>()
        val project = ProjectId(body.project)
        // Duplicate-title guard (skippable with force): 409 with the look-alikes so the caller reuses one.
        val duplicates = if (body.force) emptyList() else service.findDuplicates(project, body.title)
        if (duplicates.isNotEmpty()) {
            return@post call.respond(HttpStatusCode.Conflict, duplicates.map { it.toResponse() })
        }
        val task = service.createTask(
            projectId = project,
            title = body.title,
            goal = body.goal,
            acceptanceCriteria = body.acceptanceCriteria,
            affectedPaths = body.affectedPaths,
        )
        call.respond(HttpStatusCode.Created, task.toResponse())
    }
    // Disposable Markdown projection of the board (whole board, or one project with ?project=).
    get("/export") {
        val project = call.request.queryParameters["project"]
        val tasks = if (project != null) service.list(ProjectId(project)) else service.listAll()
        call.respondText(TaskMarkdown.render(tasks), ContentType.Text.Plain)
    }
}

// Git-driven status: a commit mention advances a task to IN_PROGRESS, a closing keyword
// ("fixes auth-12") advances it to DONE. Idempotent — wire this to a post-commit / post-merge hook
// or a CI step. Acts on commits in the body, or reads the repo's recent log when [reader] is set.
private fun Route.taskSyncRoute(service: TaskService, reader: GitCommitReader?) {
    post("/sync-git") {
        val body = runCatching { call.receive<SyncGitRequest>() }.getOrNull() ?: SyncGitRequest()
        val commits = if (body.commits.isNotEmpty()) {
            body.commits.map { Commit(it.sha, it.message) }
        } else {
            reader?.recent(body.limit) ?: emptyList()
        }
        call.respond(GitTaskSync(service).sync(commits))
    }
}

private fun Route.taskCrudRoutes(service: TaskService, assembler: TaskContextAssembler) {
    get {
        val id = taskId() ?: return@get call.respond(HttpStatusCode.BadRequest, "Missing task id")
        respondTask(service.getTask(id))
    }
    patch {
        val id = taskId() ?: return@patch call.respond(HttpStatusCode.BadRequest, "Missing task id")
        if (service.getTask(id) == null) return@patch call.respond(HttpStatusCode.NotFound, "Task not found")
        val body = call.receive<EditTaskRequest>()
        if (body.title != null || body.goal != null) service.edit(id, body.title, body.goal)
        if (body.acceptanceCriteria != null) service.setAcceptanceCriteria(id, body.acceptanceCriteria)
        if (body.affectedPaths != null) service.setAffectedPaths(id, body.affectedPaths)
        respondTask(service.getTask(id))
    }
    delete {
        val id = taskId() ?: return@delete call.respond(HttpStatusCode.BadRequest, "Missing task id")
        if (service.delete(id)) call.respond(HttpStatusCode.NoContent)
        else call.respond(HttpStatusCode.NotFound, "Task not found")
    }
    get("/context") {
        val id = taskId() ?: return@get call.respond(HttpStatusCode.BadRequest, "Missing task id")
        val bundle = assembler.assemble(id) ?: return@get call.respond(HttpStatusCode.NotFound, "Task not found")
        call.respond(bundle)
    }
    // Lifecycle audit trail from the event log; visible even after a soft-delete. Empty history
    // means the task id never existed.
    get("/history") {
        val id = taskId() ?: return@get call.respond(HttpStatusCode.BadRequest, "Missing task id")
        val events = service.history(id)
        if (events.isEmpty()) return@get call.respond(HttpStatusCode.NotFound, "Task not found")
        call.respondText(TaskHistory.render(events), ContentType.Text.Plain)
    }
    // Unfinished tasks this one is waiting on (its dependencies + inbound blocks).
    get("/blockers") {
        val id = taskId() ?: return@get call.respond(HttpStatusCode.BadRequest, "Missing task id")
        if (service.getTask(id) == null) return@get call.respond(HttpStatusCode.NotFound, "Task not found")
        call.respond(service.blockers(id).map { it.toResponse() })
    }
}

private fun Route.taskTransitionRoutes(service: TaskService) {
    post("/claim") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        respondTask(service.claim(id, call.receive<ClaimRequest>().claimant))
    }
    post("/release") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        respondTask(service.release(id))
    }
    post("/block") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        respondTask(service.block(id, call.receive<BlockRequest>().reason))
    }
    post("/unblock") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        respondTask(service.unblock(id))
    }
    post("/submit-for-review") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        respondTask(service.submitForReview(id))
    }
    post("/complete") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        respondTask(service.complete(id))
    }
    post("/reopen") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        respondTask(service.reopen(id))
    }
    post("/cancel") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        val reason = runCatching { call.receive<CancelRequest>() }.getOrNull()?.reason
        respondTask(service.cancel(id, reason))
    }
}

private fun Route.taskGraphRoutes(service: TaskService) {
    post("/links") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        val body = call.receive<LinkRequest>()
        val type = parseEnum<TaskLinkType>(body.type)
            ?: return@post call.respond(HttpStatusCode.BadRequest, "Invalid link type: ${body.type}")
        val kind = when (val raw = body.targetKind) {
            null -> TaskTargetKinds.infer(body.targetId)
            else -> parseEnum<TaskTargetKind>(raw)
                ?: return@post call.respond(HttpStatusCode.BadRequest, "Invalid target kind: $raw")
        }
        respondTask(service.link(id, body.targetId, type, kind))
    }
    delete("/links") {
        val id = taskId() ?: return@delete call.respond(HttpStatusCode.BadRequest, "Missing task id")
        val targetId = call.request.queryParameters["targetId"]
            ?: return@delete call.respond(HttpStatusCode.BadRequest, "Missing 'targetId' query parameter")
        val typeRaw = call.request.queryParameters["type"]
            ?: return@delete call.respond(HttpStatusCode.BadRequest, "Missing 'type' query parameter")
        val type = parseEnum<TaskLinkType>(typeRaw)
            ?: return@delete call.respond(HttpStatusCode.BadRequest, "Invalid link type: $typeRaw")
        respondTask(service.unlink(id, targetId, type))
    }
    post("/notes") {
        val id = taskId() ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing task id")
        val body = call.receive<NoteRequest>()
        val author = body.author?.let {
            parseEnum<TaskNoteAuthor>(it) ?: return@post call.respond(HttpStatusCode.BadRequest, "Invalid author: $it")
        } ?: TaskNoteAuthor.HUMAN
        respondTask(service.addNote(id, author, body.body))
    }
}

private fun RoutingContext.taskId(): TaskId? = call.parameters["id"]?.let { TypeId(it) }

private suspend fun RoutingContext.respondTask(task: Task?) {
    if (task == null) call.respond(HttpStatusCode.NotFound, "Task not found")
    else call.respond(task.toResponse())
}

private inline fun <reified T : Enum<T>> parseEnum(raw: String): T? =
    enumValues<T>().firstOrNull { it.name.equals(raw, ignoreCase = true) }

private fun Task.toResponse(): TaskResponse = TaskResponse(
    id = taskId.value,
    key = state.key,
    status = state.status.name,
    title = state.title,
    goal = state.goal,
    acceptanceCriteria = state.acceptanceCriteria,
    affectedPaths = state.affectedPaths,
    claimant = state.claimant,
    links = state.links.map { TaskLinkDto(it.targetId, it.type.name, it.targetKind.name) },
    notes = state.notes.map { TaskNoteDto(it.author.name, it.body, it.at.toString()) },
    createdAt = state.createdAt?.toString(),
    updatedAt = state.updatedAt?.toString(),
)
