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.TaskNoteAuthor
import com.correx.core.tasks.TaskService
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

/**
 * Agent-facing tool: when a write is blocked because the path is outside the claimed task's
 * affected_paths, propose widening the scope. This is the recalibration valve for the execution
 * loop — the implementer cannot grant itself more room (invariant #4): the tool is [Tier.T2], so it
 * only executes after the operator approves. On approval it unions the proposed paths into the
 * task's affected_paths ([TaskService.setAffectedPaths] → event); [TaskClaimCoordinator.activeScope]
 * re-derives the wider manifest from events on the next tool call, so the same write then passes.
 * On rejection the kernel blocks the task and the loop advances (see SessionOrchestrator).
 *
 * Operates on the session's currently-claimed task, so only the paths to add are needed.
 */
class ProposeScopeTool(private val service: TaskService) : Tool, ToolExecutor {

    override val name: String = "propose_scope"
    override val description: String =
        "Propose widening the current task's affected_paths when a write is blocked for being out " +
            "of scope. The operator must approve. On approval the paths are added and the blocked " +
            "write can proceed; on rejection the task is blocked. Give the paths to add and why."
    override val parametersSchema: JsonObject = buildJsonObject {
        put("type", "object")
        putJsonObject("properties") {
            putJsonObject("paths") {
                put("type", "array")
                putJsonObject("items") { put("type", "string") }
                put("description", "Path globs to add to the task's affected_paths.")
            }
            putJsonObject("reason") {
                put("type", "string")
                put("description", "Why the wider scope is needed — recorded on the task.")
            }
        }
        put("required", buildJsonArray { add(JsonPrimitive("paths")) })
    }
    override val tier: Tier = Tier.T2
    override val requiredCapabilities: Set<ToolCapability> = emptySet()

    override fun validateRequest(request: ToolRequest): ValidationResult =
        if (request.listParam("paths").isEmpty()) {
            ValidationResult.Invalid("Missing 'paths' (non-empty string array).")
        } else {
            ValidationResult.Valid
        }

    override suspend fun execute(request: ToolRequest): ToolResult {
        val task = service.activeClaim(request.sessionId.value)
            ?: return fail(request, "No claimed task for this session — nothing to widen scope for.")
        val add = request.listParam("paths")
        if (add.isEmpty()) return fail(request, "Missing 'paths' (non-empty string array).")

        val widened = (task.state.affectedPaths + add).distinct()
        service.setAffectedPaths(task.taskId, widened)
        val reason = request.stringParam("reason")
        service.addNote(
            task.taskId,
            TaskNoteAuthor.AGENT,
            "[scope] widened +$add${reason?.let { ": $it" }.orEmpty()}",
        )
        return ToolResult.Success(
            invocationId = request.invocationId,
            output = "Scope of ${task.taskId.value} widened to $widened — retry the blocked write.",
            metadata = mapOf("taskId" to task.taskId.value),
        )
    }

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