package dev.ccpocket.daemon.disk

import dev.ccpocket.protocol.DirectoryEntry
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.bufferedReader
import kotlin.io.path.exists
import kotlin.io.path.getLastModifiedTime
import kotlin.io.path.isDirectory
import kotlin.io.path.isReadable
import kotlin.io.path.isWritable

/** Enumerate candidate working directories + validate a chosen cwd. M0 = in-memory recents. */
class DirectoryService {
    private val json = Json { ignoreUnknownKeys = true; isLenient = true }
    private val recents = LinkedHashSet<String>()

    private companion object { const val ACTIVE_WINDOW_MS = 30_000L } // wrote within 30s = actively executing

    fun noteRecent(workdir: String) {
        recents.remove(workdir)
        recents.add(workdir)
        while (recents.size > 10) {
            val it = recents.iterator()
            it.next()
            it.remove()
        }
    }

    /**
     * M0: list directories that have Claude history (from ~/.claude/projects), recents flagged.
     * We read the authoritative `cwd` from each project's newest .jsonl rather than decoding the
     * (lossy) dir-key.
     */
    fun listDirectories(root: String?, busyCwds: Set<String> = emptySet()): List<DirectoryEntry> {
        val projects = ProjectPaths.projectsRoot()
        if (!projects.isDirectory()) return emptyList()
        val dirs = Files.newDirectoryStream(projects).use { it.toList() }.filter { it.isDirectory() }
        val now = System.currentTimeMillis()
        // open = a claude process is alive here (idle or active); executing = open AND wrote recently;
        // busy = a daemon conversation here has running background work (keep it "active" even when idle).
        val liveCwds = LiveProcesses.claudeCwds()
        return dirs.mapNotNull { dir -> scanProject(dir) }
            .map { (cwd, mtime, newest) ->
                val open = cwd in liveCwds
                // for live dirs, surface the active session (the newest transcript) so the row links straight into it
                val active = if (open) runCatching { TranscriptScanner.summarize(newest) }.getOrNull() else null
                DirectoryEntry(
                    path = cwd,
                    name = Path.of(cwd).fileName?.toString() ?: cwd,
                    isDir = true,
                    hasSessions = true,
                    recent = cwd in recents,
                    lastModified = mtime,
                    open = open,
                    executing = open && now - mtime < ACTIVE_WINDOW_MS,
                    busy = cwd in busyCwds,
                    activeSessionId = active?.sessionId,
                    activeSessionTitle = active?.title,
                    gitBranch = active?.gitBranch,
                )
            }
            .sortedByDescending { it.lastModified }
            .distinctBy { it.path } // keep the newest entry per cwd
    }

    /** The dir's `cwd` (from its newest transcript), that transcript's mtime, and the transcript file. */
    private fun scanProject(projectDir: Path): Triple<String, Long, Path>? {
        val newest = Files.newDirectoryStream(projectDir, "*.jsonl").use { it.toList() }
            .maxByOrNull { it.getLastModifiedTime().toMillis() } ?: return null
        val mtime = newest.getLastModifiedTime().toMillis()
        newest.bufferedReader().useLines { lines ->
            for (raw in lines) {
                val obj = runCatching { json.parseToJsonElement(raw.trim()) }.getOrNull() as? JsonObject ?: continue
                (obj["cwd"] as? JsonPrimitive)?.contentOrNull?.let { return Triple(it, mtime, newest) }
            }
        }
        return null
    }

    fun validateWorkdir(path: String): Path? {
        val p = runCatching { Path.of(path).toRealPath() }.getOrNull() ?: return null
        return if (p.isDirectory() && p.isReadable()) p else null
    }

    /**
     * Like [validateWorkdir], but for STARTING A NEW PROJECT (issue #7 follow-up): if [path] doesn't exist yet,
     * create it as a single new leaf directory under an already-existing, writable parent, then return its real
     * path. Only one level is created (no `mkdir -p` of a deep tree) and the parent must already be a writable
     * directory — so a typo'd path fails fast instead of materialising a stray tree. An existing readable
     * directory behaves exactly like [validateWorkdir]; returns null when the path is unusable (it exists but
     * isn't a readable dir, the parent is missing or not writable, or creation failed).
     *
     * A paired phone can already create folders through the approval-gated terminal (issue #3), so creating the
     * one named project directory here adds no new trust boundary.
     */
    fun validateOrCreateWorkdir(path: String): Path? {
        validateWorkdir(path)?.let { return it }                       // already a readable directory → done
        val raw = runCatching { Path.of(path).normalize() }.getOrNull() ?: return null
        if (raw.exists()) return null                                  // exists but not a readable dir (e.g. a file)
        val leaf = raw.fileName ?: return null                         // need a leaf name to create
        val parent = raw.parent?.let { p -> runCatching { p.toRealPath() }.getOrNull() } ?: return null
        if (!parent.isDirectory() || !parent.isWritable()) return null // parent must already exist & be writable
        return runCatching { Files.createDirectory(parent.resolve(leaf)).toRealPath() }.getOrNull()
    }
}
