package com.correx.infrastructure.tools.task

import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolRequest
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.TaskService
import com.correx.core.tasks.TaskTargetKinds
import com.correx.core.tools.contract.Tool
import com.correx.core.tools.contract.ToolCapability
import com.correx.core.tools.contract.ToolExecutor
import com.correx.core.tools.contract.ToolResult
import com.correx.core.tools.contract.ValidationResult
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import java.nio.file.FileSystems
import java.nio.file.Path

/**
 * Agent-facing tool: update an existing task — edit fields, transition status, add a work-graph
 * link, or attach a note. The task's project is derived from its id, so only the id is required.
 *
 * [sessionFacts] records the session's active task on claim; [sessionWrites], when bound, powers
 * the cite-before-claim gate (completing a task that declared affected_paths but saw no matching
 * write this session is blocked, escapable with force).
 */
class TaskUpdateTool(
    private val service: TaskService,
    private val sessionFacts: SessionFactRecorder? = null,
    private val sessionWrites: SessionWrites? = null,
) : Tool, ToolExecutor {

    override val name: String = "task_update"
    override val description: String =
        "Keep a task's state current as you work it: claim before starting, submit_for_review " +
            "when ready, complete after review, block/unblock when waiting on something. Also " +
            "edits title/goal/criteria/paths, adds a work-graph link, or attaches a note. " +
            "Actions: claim, release, block, unblock, submit_for_review, complete, reopen, cancel."
    override val parametersSchema: JsonObject = buildJsonObject {
        put("type", "object")
        putJsonObject("properties") {
            putJsonObject("id") { put("type", "string"); put("description", "Task id, e.g. 'auth-142'.") }
            putJsonObject("title") { put("type", "string"); put("description", "New title.") }
            putJsonObject("goal") { put("type", "string"); put("description", "New goal.") }
            putJsonObject("acceptance_criteria") {
                put("type", "array")
                putJsonObject("items") { put("type", "string") }
                put("description", "Replaces the acceptance criteria.")
            }
            putJsonObject("affected_paths") {
                put("type", "array")
                putJsonObject("items") { put("type", "string") }
                put("description", "Replaces the affected paths.")
            }
            putJsonObject("action") {
                put("type", "string")
                put(
                    "description",
                    "Status transition: claim, release, block, unblock, submit_for_review, " +
                        "complete, reopen, cancel.",
                )
            }
            putJsonObject("claimant") { put("type", "string"); put("description", "Who claims it (for action=claim).") }
            putJsonObject("reason") { put("type", "string"); put("description", "Reason (for action=block/cancel).") }
            putJsonObject("link_target") {
                put("type", "string")
                put("description", "Id to link to (task/artifact/session).")
            }
            putJsonObject("link_type") {
                put("type", "string")
                put("description", "DEPENDS_ON, BLOCKS, IMPLEMENTS, RELATES_TO, PRODUCED, CONTEXT. Default RELATES_TO.")
            }
            putJsonObject("link_kind") {
                put("type", "string")
                put(
                    "description",
                    "What the link points at: TASK, DOC, ARTIFACT, SESSION. Omit to infer from the target id.",
                )
            }
            putJsonObject("note") { put("type", "string"); put("description", "Append an agent note.") }
            putJsonObject("force") {
                put("type", "boolean")
                put(
                    "description",
                    "Override a gate (claim over unmet blockers; complete with no in-scope writes). Default false.",
                )
            }
            putJsonObject("force_reason") {
                put("type", "string")
                put(
                    "description",
                    "Required when force=true: why the override is justified. Recorded as a note on the task.",
                )
            }
        }
        put("required", buildJsonArray { add(JsonPrimitive("id")) })
    }
    override val tier: Tier = Tier.T2
    override val requiredCapabilities: Set<ToolCapability> = emptySet()

    override fun validateRequest(request: ToolRequest): ValidationResult {
        request.stringParam("id") ?: return ValidationResult.Invalid("Missing 'id' (string).")
        return ValidationResult.Valid
    }

    override suspend fun execute(request: ToolRequest): ToolResult {
        val id = request.stringParam("id")
            ?: return fail(request, "Missing 'id' (string).")
        val taskId = TaskId(id)
        if (service.getTask(taskId) == null) return fail(request, "No such task: $id")

        val action = request.stringParam("action")
        forceReasonGate(request)?.let { return it }
        claimBlock(request, taskId, action)?.let { return it }
        completeBlock(request, taskId, action)?.let { return it }

        applyFieldEdits(request, taskId)

        if (action != null && !applyAction(taskId, action, request)) {
            return fail(request, "Unknown action '$action'.")
        }

        val linkTarget = request.stringParam("link_target")
        if (linkTarget != null) {
            val type = parseLinkType(request.stringParam("link_type"))
                ?: return fail(request, "Invalid 'link_type'.")
            val kind = resolveTargetKind(request.stringParam("link_kind"), linkTarget)
                ?: return fail(request, "Invalid 'link_kind'.")
            service.link(taskId, linkTarget, type, kind)
        }

        request.stringParam("note")?.let { service.addNote(taskId, TaskNoteAuthor.AGENT, it) }

        recordForceNote(request, taskId, action)
        recordWorkingTask(request, taskId, action)

        val status = service.getTask(taskId)?.state?.status?.name ?: "UNKNOWN"
        val warning = if (action == "claim") claimWarning(taskId) else ""
        return ToolResult.Success(
            invocationId = request.invocationId,
            output = "Updated $id [$status]$warning",
            metadata = mapOf("taskId" to id, "status" to status),
        )
    }

    /** A force override must carry a recorded justification — reject force=true with no force_reason. */
    private fun forceReasonGate(request: ToolRequest): ToolResult.Failure? =
        if (request.boolParam("force") && request.stringParam("force_reason") == null) {
            fail(request, "force=true requires 'force_reason' explaining the override.")
        } else {
            null
        }

    /** A forced claim/complete bypassed a gate — record why on the task, for the audit trail. */
    private suspend fun recordForceNote(request: ToolRequest, taskId: TaskId, action: String?) {
        if (!request.boolParam("force")) return
        val reason = request.stringParam("force_reason") ?: return
        service.addNote(taskId, TaskNoteAuthor.AGENT, "[force] ${action ?: "update"}: $reason")
    }

    /** Refuse to claim a task with unmet blockers (escapable with force+reason) — fail before any mutation. */
    private fun claimBlock(request: ToolRequest, taskId: TaskId, action: String?): ToolResult.Failure? {
        if (action != "claim" || request.boolParam("force")) return null
        val unmet = service.blockers(taskId)
        if (unmet.isEmpty()) return null
        val listed = unmet.joinToString(", ") { "${it.taskId.value} [${it.state.status.name}]" }
        return fail(
            request,
            "Cannot claim ${taskId.value} — unmet blockers: $listed. Complete them, or force=true with force_reason.",
        )
    }

    /**
     * Cite-before-claim: refuse to complete a task that declared affected_paths but saw no matching
     * write this session — "marked done without doing it." Off when no [sessionWrites] is bound or
     * the task declared no scope; escapable with force.
     */
    private fun completeBlock(request: ToolRequest, taskId: TaskId, action: String?): ToolResult.Failure? {
        if (action != "complete" || request.boolParam("force")) return null
        val writes = sessionWrites ?: return null
        val scope = service.getTask(taskId)?.state?.affectedPaths.orEmpty()
        if (scope.isEmpty()) return null
        if (matchesAnyGlob(writes.pathsWritten(request.sessionId), scope)) return null
        return fail(
            request,
            "Cannot complete ${taskId.value} — no changes under its affected_paths ($scope) this " +
                "session. Make the change, or force=true with force_reason.",
        )
    }

    /** True if any written path matches any affected_paths glob (workspace-relative; './' tolerated). */
    private fun matchesAnyGlob(written: Set<String>, globs: List<String>): Boolean {
        if (written.isEmpty()) return false
        val fs = FileSystems.getDefault()
        val matchers = globs.map { fs.getPathMatcher("glob:${it.removePrefix("./")}") }
        return written.any { w ->
            val rel = Path.of(w.removePrefix("./"))
            matchers.any { it.matches(rel) }
        }
    }

    /**
     * Record the session→task working relationship (for SessionContext.activeTask) on a claim, or on
     * an affected_paths edit by the recorded claimant (to refresh the snapshot scope). No-op without
     * a recorder bound.
     */
    private suspend fun recordWorkingTask(request: ToolRequest, taskId: TaskId, action: String?) {
        val recorder = sessionFacts ?: return
        val task = service.getTask(taskId) ?: return
        val isClaim = action == "claim"
        val scopeEditByClaimant = request.parameters.containsKey("affected_paths") &&
            task.state.claimant == request.sessionId.value
        if (isClaim || scopeEditByClaimant) {
            recorder.recordWorkingTask(request.sessionId, taskId, task.state.affectedPaths)
        }
    }

    /** When a force-claimed task still has unmet blockers, flag it so the agent knows it jumped a dependency. */
    private fun claimWarning(taskId: TaskId): String {
        val blockers = service.blockers(taskId)
        if (blockers.isEmpty()) return ""
        val listed = blockers.joinToString(", ") { "${it.taskId.value} [${it.state.status.name}]" }
        return " — WARNING: claimed despite unmet blockers: $listed"
    }

    private suspend fun applyFieldEdits(request: ToolRequest, taskId: TaskId) {
        val title = request.stringParam("title")
        val goal = request.stringParam("goal")
        if (title != null || goal != null) service.edit(taskId, title, goal)
        if (request.parameters.containsKey("acceptance_criteria")) {
            service.setAcceptanceCriteria(taskId, request.listParam("acceptance_criteria"))
        }
        if (request.parameters.containsKey("affected_paths")) {
            service.setAffectedPaths(taskId, request.listParam("affected_paths"))
        }
    }

    private suspend fun applyAction(taskId: TaskId, action: String, request: ToolRequest): Boolean {
        val claimant = request.stringParam("claimant") ?: request.sessionId.value
        val reason = request.stringParam("reason")
        when (action) {
            "claim" -> {
                service.claim(taskId, claimant)
                // Record the claiming session too, so a task picked up by a different run than
                // the one that created it links both into its work graph.
                service.linkOriginSession(taskId, request)
            }
            "release" -> service.release(taskId)
            "block" -> service.block(taskId, reason ?: "blocked")
            "unblock" -> service.unblock(taskId)
            "submit_for_review" -> service.submitForReview(taskId)
            "complete" -> service.complete(taskId)
            "reopen" -> service.reopen(taskId)
            "cancel" -> service.cancel(taskId, reason)
            else -> return false
        }
        return true
    }

    private fun parseLinkType(raw: String?): TaskLinkType? =
        if (raw == null) TaskLinkType.RELATES_TO
        else TaskLinkType.entries.firstOrNull { it.name == raw.uppercase() }

    /** Explicit kind when given (null if unrecognised); otherwise inferred from the target id. */
    private fun resolveTargetKind(raw: String?, target: String): TaskTargetKind? =
        if (raw != null) TaskTargetKind.entries.firstOrNull { it.name == raw.uppercase() }
        else TaskTargetKinds.infer(target)

    private fun fail(request: ToolRequest, reason: String): ToolResult.Failure =
        ToolResult.Failure(request.invocationId, reason, recoverable = false)
}
