package com.correx.infrastructure.tools.filesystem

import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.tools.contract.FileAffectingTool
import com.correx.core.tools.contract.ParamRole
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.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
import java.nio.file.Path
import java.nio.file.Paths

class FileWriteTool(
    allowedPaths: Set<Path> = emptySet(),
    private val workingDir: Path? = null,
) : Tool, FileAffectingTool, ToolExecutor {

    private val normalizedAllowedPaths: Set<Path> = allowedPaths.map { it.normalize().toAbsolutePath() }.toSet()

    override val name: String = "file_write"
    override val description: String = "Write content to a file at the specified path or delete the file entirely"
    override val parametersSchema: JsonObject = buildJsonObject {
        put("type", "object")
        putJsonObject("properties") {
            putJsonObject("path") {
                put("type", "string")
                put("description", "Relative path to write to")
            }
            putJsonObject("content") {
                put("type", "string")
                put("description", "File content")
            }
            putJsonObject("operation") {
                put("type", "string")
                put("description", "Either 'write' or 'delete'")
            }
        }
        put(
            "required",
            buildJsonArray {
                add(JsonPrimitive("path"))
                add(JsonPrimitive("content"))
                add(JsonPrimitive("operation"))
            },
        )
    }
    override val tier: Tier = Tier.T2
    override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE)
    override val paramRoles: Map<String, ParamRole> = mapOf("path" to ParamRole.PATH)

    override fun affectedPaths(request: ToolRequest): Set<Path> {
        val pathString = request.parameters["path"] as? String ?: return emptySet()
        return setOf(resolvePath(pathString))
    }

    private fun resolvePath(pathString: String): Path {
        val raw = Paths.get(pathString)
        return when {
            raw.isAbsolute -> raw.normalize()
            workingDir != null -> workingDir.resolve(raw).normalize()
            else -> raw.toAbsolutePath().normalize()
        }
    }

    override fun validateRequest(request: ToolRequest): ValidationResult {
        val operation = request.parameters["operation"] as? String
        val pathString = request.parameters["path"] as? String

        return when {
            operation == null ->
                ValidationResult.Invalid("Missing 'operation' parameter. Expected 'write' or 'delete'.")

            pathString == null ->
                ValidationResult.Invalid("Missing 'path' parameter. Expected String.")

            else -> runCatching {
                val path = resolvePath(pathString)
                checkPathAllowed(path, pathString, operation, request)
            }.getOrElse { e ->
                mapExceptionToValidationResult(e)
            }
        }
    }

    private fun checkPathAllowed(
        path: Path,
        pathString: String,
        operation: String,
        request: ToolRequest,
    ): ValidationResult {
        return when {
            normalizedAllowedPaths.isEmpty() -> ValidationResult.Invalid("No paths are allowed.")
            !isPathAllowed(path) ->
                ValidationResult.Invalid("Path '$pathString' is not in the allowed list.")

            operation == "write" && !request.parameters.containsKey("content") ->
                ValidationResult.Invalid("Missing 'content' parameter for 'write' operation.")

            operation != "write" && operation != "delete" ->
                ValidationResult.Invalid("Unknown operation: $operation")

            else -> ValidationResult.Valid
        }
    }

    private fun isPathAllowed(path: Path): Boolean =
        PathJail.isContained(path, normalizedAllowedPaths)

    private fun mapExceptionToValidationResult(e: Throwable): ValidationResult =
        when (e) {
            is InvalidPathException -> ValidationResult.Invalid("Invalid path format: ${e.message}")
            is IOException -> ValidationResult.Invalid("IO error: ${e.message}")
            is SecurityException -> ValidationResult.Invalid("Security error: ${e.message}")
            else -> ValidationResult.Invalid(e.message ?: "Unknown error occurred")
        }

    override suspend fun execute(request: ToolRequest): ToolResult = withContext(Dispatchers.IO) {
        val validation = validateRequest(request)
        if (validation is ValidationResult.Invalid) {
            return@withContext ToolResult.Failure(
                invocationId = request.invocationId,
                reason = validation.reason,
                recoverable = false,
            )
        }

        val operation = request.parameters["operation"] as String
        val pathString = request.parameters["path"] as String
        val path = resolvePath(pathString)
        val originalContent = runCatching { Files.readString(path) }.getOrDefault("")

        val result = runCatching {
            performOperation(operation, path, pathString, request)
        }.getOrElse { e ->
            handleExecutionException(e, request.invocationId, pathString)
        }

        // Attach a unified diff of the change so the side effect is auditable downstream — the
        // reviewer stage reads the file_written artifact and needs to see WHAT changed, not just
        // that a write happened (F-019).
        if (result is ToolResult.Success) {
            val updatedContent = runCatching { Files.readString(path) }.getOrDefault("")
            val diff = unifiedDiff(pathString, originalContent, updatedContent)
            result.copy(
                output = if (diff.isBlank()) result.output else "${result.output}\n\n$diff",
                metadata = result.metadata + ("diff" to diff),
            )
        } else {
            result
        }
    }

    private suspend fun performOperation(
        operation: String,
        path: Path,
        pathString: String,
        request: ToolRequest,
    ): ToolResult = when (operation) {
        "write" -> {
            val content = request.parameters["content"] as String
            withContext(Dispatchers.IO) {
                AtomicFileWriter.write(path, content.toByteArray(Charsets.UTF_8))
            }
            ToolResult.Success(
                invocationId = request.invocationId,
                output = "File written successfully to $pathString",
            )
        }

        "delete" -> {
            if (Files.exists(path)) {
                withContext(Dispatchers.IO) {
                    Files.deleteIfExists(path)
                }
                ToolResult.Success(
                    invocationId = request.invocationId,
                    output = "File deleted successfully: $pathString",
                )
            } else {
                ToolResult.Failure(
                    invocationId = request.invocationId,
                    reason = "File not found: $pathString",
                    recoverable = false,
                )
            }
        }

        else -> ToolResult.Failure(
            invocationId = request.invocationId,
            reason = "Unknown operation: $operation",
            recoverable = false,
        )
    }

    private fun handleExecutionException(
        e: Throwable,
        invocationId: ToolInvocationId,
        pathString: String,
    ): ToolResult = when (e) {
        is CancellationException -> throw e
        is IOException -> ToolResult.Failure(
            invocationId = invocationId,
            reason = "IO error: ${e.message}",
            recoverable = false,
        )

        is SecurityException -> ToolResult.Failure(
            invocationId = invocationId,
            reason = "Access denied: $pathString, ${e.message}",
            recoverable = false,
        )

        else -> ToolResult.Failure(
            invocationId = invocationId,
            reason = e.message ?: "Unknown error occurred",
            recoverable = false,
        )
    }
}
