package com.correx.apps.server.memory

import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import java.security.MessageDigest
import kotlin.io.path.extension
import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
import kotlin.io.path.relativeTo

private val log = LoggerFactory.getLogger(RealWorkspaceStateProbe::class.java)

private const val FINGERPRINT_PREFIX = "fp:"
private const val GIT_PREFIX = "git:"
private const val SOURCE_GIT = "git"
private const val SOURCE_FINGERPRINT = "fingerprint"
private const val FINGERPRINT_HEX_LENGTH = 16
private const val MAX_DEPTH = 4

/** Immutable snapshot of a workspace content state. */
data class WorkspaceState(
    val stateKey: String,
    val source: String,
    val branch: String?,
    val dirty: Boolean,
)

/** Produces a [WorkspaceState] for any workspace (git or plain directory). */
interface WorkspaceStateProbe {
    fun observe(workspaceRoot: String): WorkspaceState?
}

/** Test double returning a fixed result. */
class FakeWorkspaceStateProbe(private val result: WorkspaceState?) : WorkspaceStateProbe {
    override fun observe(workspaceRoot: String): WorkspaceState? = result
}

/**
 * Production probe. For a clean git worktree the stateKey is "git:<HEAD>"; for a dirty
 * worktree it is "fp:<fingerprint>"; for a non-git directory it is "fp:<fingerprint>".
 * Everything is wrapped in runCatching — any failure returns null.
 */
class RealWorkspaceStateProbe : WorkspaceStateProbe {

    override fun observe(workspaceRoot: String): WorkspaceState? =
        runCatching { doObserve(workspaceRoot) }
            .onFailure { log.debug("workspace state probe failed for {}: {}", workspaceRoot, it.message) }
            .getOrNull()

    private fun doObserve(workspaceRoot: String): WorkspaceState? {
        val root = Path.of(workspaceRoot)
        if (!root.isDirectory()) return null

        val gitInfo = tryGit(root)
        return if (gitInfo != null) {
            val (head, branch, dirty) = gitInfo
            if (dirty) {
                val fp = fingerprint(root)
                WorkspaceState("$FINGERPRINT_PREFIX$fp", SOURCE_FINGERPRINT, branch, dirty = true)
            } else {
                WorkspaceState("$GIT_PREFIX$head", SOURCE_GIT, branch, dirty = false)
            }
        } else {
            val fp = fingerprint(root)
            WorkspaceState("$FINGERPRINT_PREFIX$fp", SOURCE_FINGERPRINT, branch = null, dirty = false)
        }
    }

    /**
     * Returns (head, branch, dirty) or null if the directory is not a git repo or any
     * subprocess call fails.
     */
    private fun tryGit(root: Path): Triple<String, String?, Boolean>? {
        val head = runProcess(root, "git", "rev-parse", "HEAD") ?: return null
        val branch = runProcess(root, "git", "rev-parse", "--abbrev-ref", "HEAD")
            ?.takeIf { it.isNotBlank() && it != "HEAD" }
        val statusOutput = runProcess(root, "git", "status", "--porcelain") ?: return null
        val dirty = statusOutput.isNotBlank()
        return Triple(head, branch, dirty)
    }

    /** Runs a process in [dir]; returns trimmed stdout or null on non-zero exit / error. */
    private fun runProcess(dir: Path, vararg command: String): String? =
        runCatching {
            val result = ProcessBuilder(*command)
                .directory(dir.toFile())
                .redirectErrorStream(true)
                .start()
            val output = result.inputStream.bufferedReader().readText().trim()
            if (result.waitFor() != 0) null else output
        }.getOrNull()

    /**
     * SHA-256 over sorted "relativePath|mtimeMs|size" lines, using the same depth and
     * file-extension filter as [RepoMapIndexer].
     */
    private fun fingerprint(root: Path): String {
        val indexedExtensions = RepoMapIndexer.INDEXED_EXTENSIONS
        val lines = Files.walk(root, MAX_DEPTH).use { stream ->
            stream.filter { it.isRegularFile() && it.extension in indexedExtensions }
                .map { path ->
                    val rel = path.relativeTo(root).toString()
                    val mtime = runCatching { Files.getLastModifiedTime(path).toMillis() }.getOrDefault(0L)
                    val size = runCatching { Files.size(path) }.getOrDefault(0L)
                    "$rel|$mtime|$size"
                }
                .sorted()
                .toList()
        }
        val digest = MessageDigest.getInstance("SHA-256")
        digest.update(lines.joinToString("\n").toByteArray())
        return digest.digest().joinToString("") { "%02x".format(it) }.take(FINGERPRINT_HEX_LENGTH)
    }
}
