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.GitLogResult
import wang.yaojia.webterm.api.models.PrStatus
import wang.yaojia.webterm.api.models.ProjectDetail
import wang.yaojia.webterm.api.routes.ApiClientError

/**
 * # ProjectDetailViewModel (A23 + W5) — one project's detail (`GET /projects/detail?path=`), a phase
 * state machine, PLUS two failure-ISOLATED side fetches: the PR + CI chip (`GET /projects/pr`) and the
 * recent-commits list (`GET /projects/log`). A failure of either side fetch NEVER fails the detail load
 * (each has its own StateFlow) — the chip/list simply render an unavailable state.
 *
 * The main [fetch] closure is injected (production wraps [ProjectsGateway.projectDetail]); [fetchPr] /
 * [fetchLog] are optional side fetches (null → the chip/list stay [PrChip.Hidden] / [RecentCommits.Hidden]).
 * [worktree] (when wired) drives the guarded create/remove/prune actions and re-fetches this detail on
 * success (via [load]).
 *
 * A plain presenter (not `androidx.lifecycle.ViewModel`) so it runs under `runTest`. The screen calls
 * [load] in a lifecycle scope; the retry action re-calls it.
 */
public class ProjectDetailViewModel(
    public val path: String,
    private val fetch: suspend () -> ProjectDetail,
    private val fetchPr: (suspend () -> PrStatus)? = null,
    private val fetchLog: (suspend () -> GitLogResult)? = null,
    /** Guarded worktree actions bound to this project; null when the gateway isn't wired (tests). */
    public val worktree: WorktreeViewModel? = null,
) {
    /** User-visible failure buckets (copy mapped in `ProjectDetailScreen`). */
    public enum class Failure { PATH_INVALID, NOT_FOUND, UNAVAILABLE }

    /** The load phase the screen renders (spinner / detail / error+retry). */
    public sealed interface Phase {
        public data object Loading : Phase
        public data class Loaded(val detail: ProjectDetail) : Phase
        public data class Failed(val failure: Failure) : Phase
    }

    /** The PR + CI chip's own state (isolated from the detail load). */
    public sealed interface PrChip {
        public data object Hidden : PrChip
        public data object Loading : PrChip
        public data class Loaded(val status: PrStatus) : PrChip
        public data object Unavailable : PrChip
    }

    /** The recent-commits section's own state (isolated from the detail load). */
    public sealed interface RecentCommits {
        public data object Hidden : RecentCommits
        public data object Loading : RecentCommits
        public data class Loaded(val result: GitLogResult) : RecentCommits
        public data object Unavailable : RecentCommits
    }

    private val _phase = MutableStateFlow<Phase>(Phase.Loading)
    private val _prChip = MutableStateFlow<PrChip>(PrChip.Hidden)
    private val _recentCommits = MutableStateFlow<RecentCommits>(RecentCommits.Hidden)

    /** The main detail snapshot `ProjectDetailScreen` renders from. */
    public val phase: StateFlow<Phase> = _phase.asStateFlow()

    /** The PR chip snapshot (renders one chip from [PrStatus.availability]). */
    public val prChip: StateFlow<PrChip> = _prChip.asStateFlow()

    /** The recent-commits snapshot. */
    public val recentCommits: StateFlow<RecentCommits> = _recentCommits.asStateFlow()

    /**
     * Fetch and present. Also the retry path: callable again after a [Phase.Failed]. On a successful
     * detail load it runs the two side fetches, each failure-isolated (a PR/log failure does not fail
     * the detail load nor each other).
     */
    public suspend fun load() {
        _phase.value = Phase.Loading
        _phase.value = try {
            Phase.Loaded(fetch())
        } catch (cancel: CancellationException) {
            throw cancel
        } catch (error: ApiClientError) {
            Phase.Failed(failureFor(error))
        } catch (_: Throwable) {
            // Transport/decode etc. — a retryable catch-all.
            Phase.Failed(Failure.UNAVAILABLE)
        }
        if (_phase.value is Phase.Loaded) {
            loadPr()
            loadRecentCommits()
        }
    }

    private suspend fun loadPr() {
        val fetcher = fetchPr ?: return
        _prChip.value = PrChip.Loading
        _prChip.value = try {
            PrChip.Loaded(fetcher())
        } catch (cancel: CancellationException) {
            throw cancel
        } catch (_: Throwable) {
            PrChip.Unavailable // isolated: a PR fetch failure never touches the detail phase
        }
    }

    private suspend fun loadRecentCommits() {
        val fetcher = fetchLog ?: return
        _recentCommits.value = RecentCommits.Loading
        _recentCommits.value = try {
            RecentCommits.Loaded(fetcher())
        } catch (cancel: CancellationException) {
            throw cancel
        } catch (_: Throwable) {
            RecentCommits.Unavailable
        }
    }

    private fun failureFor(error: ApiClientError): Failure = when (error) {
        ApiClientError.ProjectPathInvalid, ApiClientError.InvalidRequest -> Failure.PATH_INVALID
        ApiClientError.ProjectNotFound -> Failure.NOT_FOUND
        else -> Failure.UNAVAILABLE
    }

    public companion object {
        /**
         * Production assembly seam ([ProjectsViewModel.makeDetailViewModel] mints via this). Wires the
         * detail + PR + log fetches and a [WorktreeViewModel] whose successes re-fetch this detail.
         */
        public fun forGateway(gateway: ProjectsGateway, path: String): ProjectDetailViewModel {
            var self: ProjectDetailViewModel? = null
            val worktree = WorktreeViewModel(gateway, path, onChanged = { self?.load() })
            val vm = ProjectDetailViewModel(
                path = path,
                fetch = { gateway.projectDetail(path) },
                fetchPr = { gateway.projectPr(path) },
                fetchLog = { gateway.projectLog(path, null) },
                worktree = worktree,
            )
            self = vm
            return vm
        }
    }
}
