package wang.yaojia.webterm.viewmodels

import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import wang.yaojia.webterm.api.models.CreateWorktreeResult
import wang.yaojia.webterm.api.models.GitWriteOutcome
import wang.yaojia.webterm.api.models.PruneWorktreesResult
import wang.yaojia.webterm.api.models.RemoveWorktreeResult
import wang.yaojia.webterm.api.models.WorktreeInfo

/**
 * # WorktreeViewModel (W5) — the guarded worktree write actions for one project.
 *
 * A phase machine (`Idle → Working → Done | Failed`) over the three guarded routes
 * (`POST /projects/worktree`, `DELETE /projects/worktree`, `POST /projects/worktree/prune`), all
 * flowing through the :api-client Origin-stamping point (plan §Security). On a successful op it invokes
 * [onChanged] so the detail screen re-fetches and the worktree list refreshes.
 *
 * ### Defense in depth (UX, not the security boundary)
 * The branch name is pre-validated client-side ([isValidBranchName], a mirror of the server's
 * `validateBranchName`) so an obviously bad name fails with NO network I/O; a **main** worktree removal
 * is blocked client-side ([WorktreeInfo.isMain]) — the server re-validates + realpath-contains
 * regardless. Server `error` strings (disabled kill-switch, "uncommitted changes; force required") are
 * surfaced INERT (plain text; never linkified).
 *
 * A plain presenter (not `androidx.lifecycle.ViewModel`) so it runs under `runTest`. The screen calls
 * the suspend actions from a lifecycle scope; [reset] clears a settled banner back to [Phase.Idle].
 */
public class WorktreeViewModel(
    private val gateway: ProjectsGateway,
    private val repoPath: String,
    /** Invoked after any successful write so the detail page re-fetches (list refresh). */
    private val onChanged: suspend () -> Unit = {},
) {
    /** The action phase the screen renders (idle / spinner / success banner / failure banner). */
    public sealed interface Phase {
        public data object Idle : Phase
        public data object Working : Phase
        public data class Done(val message: String) : Phase
        public data class Failed(val message: String) : Phase
    }

    private val _phase = MutableStateFlow<Phase>(Phase.Idle)

    /** The single snapshot the worktree sheet/dialog renders from. */
    public val phase: StateFlow<Phase> = _phase.asStateFlow()

    /** Create a worktree for [branch] (off optional [base]). Invalid branch → [Phase.Failed], no I/O. */
    public suspend fun create(branch: String, base: String? = null) {
        val trimmed = branch.trim()
        if (!isValidBranchName(trimmed)) {
            _phase.value = Phase.Failed(WorktreeCopy.INVALID_BRANCH)
            return
        }
        if (_phase.value == Phase.Working) return
        _phase.value = Phase.Working
        val cleanBase = base?.trim()?.takeIf { it.isNotEmpty() }
        _phase.value = runOp { gateway.createWorktree(repoPath, trimmed, cleanBase) }
    }

    /** Remove [worktree] ([force] to discard uncommitted changes). A **main** worktree is blocked here. */
    public suspend fun remove(worktree: WorktreeInfo, force: Boolean) {
        if (worktree.isMain) {
            _phase.value = Phase.Failed(WorktreeCopy.CANNOT_REMOVE_MAIN)
            return
        }
        if (_phase.value == Phase.Working) return
        _phase.value = Phase.Working
        _phase.value = runOp { gateway.removeWorktree(repoPath, worktree.path, force) }
    }

    /** Reclaim stale worktree admin dirs (idempotent). */
    public suspend fun prune() {
        if (_phase.value == Phase.Working) return
        _phase.value = Phase.Working
        _phase.value = runOp { gateway.pruneWorktrees(repoPath) }
    }

    /** Clear a settled banner (Done/Failed) back to Idle after the user dismisses it. */
    public fun reset() {
        _phase.value = Phase.Idle
    }

    /**
     * Run one guarded write, mapping its [GitWriteOutcome] to a phase. On success it re-fetches the
     * detail (via [onChanged]) BEFORE settling to [Phase.Done] so the list is fresh when the banner shows.
     */
    private suspend fun <T> runOp(op: suspend () -> GitWriteOutcome<T>): Phase {
        val outcome = try {
            op()
        } catch (cancel: CancellationException) {
            throw cancel
        } catch (error: Throwable) {
            return Phase.Failed(WorktreeCopy.failed(error.message ?: error.toString()))
        }
        return when (outcome) {
            is GitWriteOutcome.Ok -> {
                runCatching { onChanged() } // a refresh failure must not turn a successful write into a failure
                Phase.Done(WorktreeCopy.okMessage(outcome.payload))
            }
            is GitWriteOutcome.Rejected -> Phase.Failed(outcome.message ?: WorktreeCopy.REJECTED)
            GitWriteOutcome.RateLimited -> Phase.Failed(WorktreeCopy.RATE_LIMITED)
        }
    }

    public companion object {
        /** Longest branch name the server accepts (`src/http/worktrees.ts` `MAX_BRANCH_LEN`). */
        private const val MAX_BRANCH_LEN = 250

        /** Mirror of the server's `FORBIDDEN_BRANCH_CHARS`: control/DEL, whitespace, `~^:?*[\`. */
        private val FORBIDDEN_BRANCH_CHARS = Regex("[\\u0000-\\u001f\\u007f\\s~^:?*\\[\\\\]")

        /**
         * Client-side mirror of `validateBranchName` (worktrees.ts:95) — a fast UX pre-check ONLY; the
         * server re-validates. Rejects empty/overlong, leading `-`, bad slashes, `..`, `.lock`/trailing
         * `.`, `@{`, and any forbidden char.
         */
        public fun isValidBranchName(branch: String): Boolean {
            if (branch.isEmpty() || branch.length > MAX_BRANCH_LEN) return false
            if (branch.startsWith("-")) return false
            if (branch.startsWith("/") || branch.endsWith("/") || branch.contains("//")) return false
            if (branch.contains("..")) return false
            if (branch.endsWith(".lock") || branch.endsWith(".")) return false
            if (branch.contains("@{")) return false
            if (FORBIDDEN_BRANCH_CHARS.containsMatchIn(branch)) return false
            return true
        }
    }
}

/** User-visible worktree-action copy (Chinese named constants; server strings surfaced inert). */
public object WorktreeCopy {
    public const val INVALID_BRANCH: String = "分支名不合法（含非法字符或格式）。"
    public const val CANNOT_REMOVE_MAIN: String = "不能删除主工作树。"
    public const val REJECTED: String = "操作被服务器拒绝。"
    public const val RATE_LIMITED: String = "操作过于频繁，服务器已限流，请稍后再试。"

    public fun failed(detail: String): String = "工作树操作失败：$detail"

    public fun okMessage(payload: Any?): String = when (payload) {
        is CreateWorktreeResult -> "已创建工作树 ${payload.branch ?: ""}".trim()
        is RemoveWorktreeResult -> "已删除工作树"
        is PruneWorktreesResult ->
            if (payload.pruned.isEmpty()) "没有可清理的工作树" else "已清理 ${payload.pruned.size} 个工作树"
        else -> "操作完成"
    }
}
