package dev.rnett.gradle.mcp.gradle.build

import dev.rnett.gradle.mcp.gradle.BuildId
import dev.rnett.gradle.mcp.gradle.GradleInvocationArguments
import dev.rnett.gradle.mcp.gradle.ProblemAggregation
import dev.rnett.gradle.mcp.gradle.ProblemSeverity
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.gradle.tooling.BuildCancelledException
import org.gradle.tooling.CancellationTokenSource
import org.gradle.tooling.GradleConnectionException
import java.nio.file.Path
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.Instant
import kotlinx.coroutines.CancellationException

/**
 * Represents a running Gradle build.
 */
class RunningBuild(
    override val id: BuildId,
    override val args: GradleInvocationArguments,
    override val startTime: Instant,
    val projectRoot: Path,
    val logBuffer: StringBuffer = StringBuffer(),
    val cancellationTokenSource: CancellationTokenSource,
    private val scope: CoroutineScope
) : Build, BuildProgressInfoProvider {

    /**
     * The progress tracker for this running build.
     */
    val progressTracker = BuildProgressTracker(this)

    /**
     * The total number of items (e.g., tasks) in the current phase.
     */
    val totalItems get() = progressTracker.totalItems

    /**
     * The number of completed items in the current phase.
     */
    val completedItems get() = progressTracker.completedItems

    /**
     * The name of the current active phase.
     */
    val currentPhase get() = progressTracker.currentPhase

    internal val problemsAccumulator = ProblemsAccumulator()
    override val problems: List<ProblemAggregation> get() = problemsAccumulator.aggregate()
    override val problemAggregations: Map<ProblemSeverity, List<ProblemAggregation>> get() = problemsAccumulator.aggregateBySeverity()
    private val indexer = FailureIndexer()
    internal val testResultsInternal = TestCollector(true, true, cancellationTokenSource.token())
    override val testResults: TestResults
        get() {
            val results = testResultsInternal.results(Clock.System.now().toEpochMilliseconds())
            val inProgressOutcome = if (status == BuildStatus.Running) BuildComponentOutcome.IN_PROGRESS else BuildComponentOutcome.CANCELLED
            return TestResults(
                results.passed.map { it.toModel(indexer, BuildComponentOutcome.SUCCESS) }.toSet(),
                results.skipped.map { it.toModel(indexer, BuildComponentOutcome.SKIPPED) }.toSet(),
                results.failed.map { it.toModel(indexer, BuildComponentOutcome.FAILED) }.toSet(),
                results.cancelled.map { it.toModel(indexer, BuildComponentOutcome.CANCELLED) }.toSet() +
                        results.inProgress.map { it.toModel(indexer, inProgressOutcome) }.toSet()
            )
        }
    val publishedScansInternal = ConcurrentLinkedQueue<GradleBuildScan>()
    override val publishedScans: List<GradleBuildScan> get() = publishedScansInternal.toList()
    override val activeOperations: List<String> get() = progressTracker.activeOperations
    override val taskResults = ConcurrentHashMap<String, TaskResult>()
    val taskOutputsAccumulator = ConcurrentHashMap<String, StringBuffer>()
    override val taskOutputs: Map<String, String> get() = taskOutputsAccumulator.mapValues { it.value.toString() }

    @Volatile
    override var taskOutputCapturingFailed: Boolean = false

    @Volatile
    var configCacheReportPointerInternal: String? = null
    override val configCacheReportPointer: String? get() = configCacheReportPointerInternal

    /**
     * The number of tests that have passed so far.
     */
    override val passedTests: Int get() = testResultsInternal.passedCount

    /**
     * The number of tests that have failed so far.
     */
    override val failedTests: Int get() = testResultsInternal.failedCount

    /**
     * The number of tests that have been skipped so far.
     */
    override val skippedTests: Int get() = testResultsInternal.skippedCount

    /**
     * The total number of tests detected so far (passed + failed + skipped + cancelled + in progress).
     */
    override val totalTests: Int get() = testResultsInternal.totalCount

    private val _logLines = MutableSharedFlow<String>(replay = 10, extraBufferCapacity = 500, onBufferOverflow = BufferOverflow.DROP_OLDEST)
    val logLines: SharedFlow<String> = _logLines.asSharedFlow()

    private val _completingTasks = MutableSharedFlow<String>(replay = 1, extraBufferCapacity = 100, onBufferOverflow = BufferOverflow.DROP_OLDEST)
    val completingTasks: SharedFlow<String> = _completingTasks.asSharedFlow()

    private val _taskPaths: MutableSet<String> = Collections.newSetFromMap(ConcurrentHashMap())
    val completedTaskPaths: Set<String> get() = _taskPaths.toSet()

    @Volatile
    override var status: BuildStatus = BuildStatus.Running
        private set

    override val consoleOutput: CharSequence get() = logBuffer.toString()

    private val finishedBuildDeferred = CompletableDeferred<FinishedBuild>()

    override suspend fun awaitFinished(): FinishedBuild {
        return finishedBuildDeferred.await()
    }

    private fun computeTaskOriginAggregation(): Map<String, Int> {
        val counts = mutableMapOf<String, Int>()
        for (result in taskResults.values) {
            val key = result.provenance ?: "_unknown"
            counts[key] = (counts[key] ?: 0) + 1
        }
        if ("_unknown" in counts && counts["_unknown"] == 0) counts.remove("_unknown")
        // Omit _unknown when unused is already satisfied by not inserting it; but if all tasks have provenance, _unknown won't be present
        return counts
    }

    override val phaseCounts: Map<String, PhaseCount> get() = progressTracker.computePhaseCounts()
    override val taskOriginAggregation: Map<String, Int> get() = computeTaskOriginAggregation()

    private fun toFinishedBuild(exception: Exception? = null): FinishedBuild {
        val buildFailures = (exception as? GradleConnectionException)?.failures?.map { indexer.withIndex(it.toContent()) }.orEmpty()
        val outcome = when {
            exception is BuildCancelledException || exception is CancellationException -> BuildOutcome.Canceled
            buildFailures.isNotEmpty() -> BuildOutcome.Failed(buildFailures)
            exception != null -> BuildOutcome.Failed(listOf(indexer.withIndex(FailureContent(exception.message, exception.toString(), emptySet(), emptyMap()))))
            else -> BuildOutcome.Success
        }

        val frozenPhaseCounts = progressTracker.computePhaseCounts()
        val frozenTaskOrigin = computeTaskOriginAggregation()
        val frozenCcPointer = configCacheReportPointerInternal

        return RefFinishedBuild(this, Clock.System.now(), outcome, frozenPhaseCounts, frozenTaskOrigin, frozenCcPointer)
    }

    fun finish(exception: Exception? = null, store: (FinishedBuild) -> Unit): FinishedBuild {
        val finished = toFinishedBuild(exception)
        this.status = finished.outcome
        store(finished)
        finishedBuildDeferred.complete(finished)
        return finished
    }

    fun stop() {
        if (hasBuildFinished) return
        testResultsInternal.isCancelled = true
        cancellationTokenSource.cancel()
    }

    internal fun addLogLine(line: String) {
        logBuffer.appendLine(line)
        _logLines.tryEmit(line)
    }

    internal fun addTaskResult(taskPath: String, outcome: BuildComponentOutcome, duration: Duration, consoleOutput: String?, provenance: String?, reason: String? = null) {
        taskResults[taskPath] = TaskResult(taskPath, outcome, duration, consoleOutput, provenance, reason)
        _taskPaths.add(taskPath)
        _completingTasks.tryEmit(taskPath)
        progressTracker.emitProgress()
    }

    internal fun addTaskCompleted(taskPath: String) {
        if (!taskResults.containsKey(taskPath)) {
            _taskPaths.add(taskPath)
            _completingTasks.tryEmit(taskPath)
            progressTracker.emitProgress()
        }
    }

    internal fun addTaskOutput(taskPath: String, output: String) {
        taskOutputsAccumulator.computeIfAbsent(taskPath) { StringBuffer() }.append(output).append('\n')
    }

}

private class RefFinishedBuild(
    val runningBuild: RunningBuild,
    override val finishTime: Instant,
    override val outcome: BuildOutcome,
    private val frozenPhaseCounts: Map<String, PhaseCount>,
    private val frozenTaskOrigin: Map<String, Int>,
    private val frozenCcPointer: String?
) : FinishedBuild, Build by runningBuild {
    override suspend fun awaitFinished(): FinishedBuild {
        return this
    }

    override val taskResults: Map<String, TaskResult> = runningBuild.taskResults.mapValues { (path, result) ->
        result.copy(consoleOutput = runningBuild.taskOutputs[path], provenance = result.provenance)
    }
    override val status: BuildStatus
        get() = runningBuild.status
    override val phaseCounts: Map<String, PhaseCount> get() = frozenPhaseCounts
    override val taskOriginAggregation: Map<String, Int> get() = frozenTaskOrigin
    override val configCacheReportPointer: String? get() = frozenCcPointer
}

private fun TestCollector.Result.toModel(indexer: FailureIndexer, status: BuildComponentOutcome): TestResult {
    return TestResult(
        testName = testName,
        suiteName = suiteName,
        consoleOutput = output,
        executionDuration = duration,
        failures = failures?.map { indexer.withIndex(it.toContent()) },
        status = status,
        metadata = metadata,
        attachments = attachments.map { TestResult.Attachment(it.file, it.mediaType) },
        taskPath = taskPath
    )
}