package twitch.instrumentorum

import jetbrains.buildServer.configs.kotlin.v2018_2.Project
import jetbrains.buildServer.configs.kotlin.v2018_2.TeamCityDsl
import jetbrains.buildServer.configs.kotlin.v2018_2.vcs.GitVcsRoot
import twitch.instrumentorum.enums.AWSRegion
import twitch.instrumentorum.enums.ProjectSetting
import twitch.instrumentorum.exceptions.AmbiguousBuildReference
import twitch.instrumentorum.helpers.nameToId
import twitch.instrumentorum.project.features.ECRConnection
import twitch.instrumentorum.project.features.ecrConnection

@TeamCityDsl
class InstrumProject(name: String?, val idNamespace: String?, val parent: InstrumProject?) :
    Project(), ParameterSupport {

    internal val buildGroups = arrayListOf<BuildGroup>()
    internal val ecrConnections = mutableMapOf<String, ECRConnection>()
    internal val projectSettings = mutableMapOf<ProjectSetting, String>()

    var sourceRootUrl = ""
    var sourceVCSRoot = GitVcsRoot()
    var settingsVCSRoot: GitVcsRoot? = null

    internal val rootProject: InstrumProject
        get() = parent?.rootProject ?: this
    internal val allProjects: List<InstrumProject>
        get() = rootProject.instrumSubProjects + rootProject
    internal val instrumSubProjects: List<InstrumProject>
        get() = subProjects.filterIsInstance<InstrumProject>() +
            subProjects.filterIsInstance<InstrumProject>().flatMap(InstrumProject::instrumSubProjects)

    init {
        if (name != null) this.name = name
        parent?.subProject(this)

        if (parent == null) {
            textParam(
                "git.branch.default",
                "refs/heads/master",
                "Default Branch",
                "What branch to treat as the default for a given build configuration."
            )
            textParam(
                "git.branch.spec",
                "+:refs/heads/*\n+:refs/tags/*\n+:refs/(pull/*)/head",
                "Branch Spec",
                "What branches should we additionally watch or ignore?"
            )
            textParam(
                "teamcity.vcsTrigger.runBuildInNewEmptyBranch",
                "true",
                "Trigger Empty Branches",
                "Should cause Teamcity to run builds when re-tagging the same commit."
            )
        }
    }

    constructor(name: String?, idNamespace: String?, parent: InstrumProject?, init: InstrumProject.() -> Unit) :
        this(name, idNamespace, parent) {

        init()

        if (parent == null) {
            finalizeBuildChain()
            finalize()
        }
    }

    internal fun finalize() {
        if (parent != null) {
            id(nameToId(name, parent))
        }

        buildTypesOrder = buildTypes
        subProjectsOrder = subProjects

        if (sourceRootUrl.isNotEmpty() && settingsVCSRoot == null) {
            settingsRoot(sourceRootUrl)
        }

        projectSettings.forEach { setting, value -> setting.addParameter(this, value) }
        buildGroups.forEach { it.finalize() }
        subProjects.filterIsInstance<InstrumProject>().forEach { it.finalize() }
    }

    internal fun finalizeBuildChain(chainFrom: BuildGroup? = null) {
        var previousGroup = chainFrom
        buildGroups.forEach { group ->
            group.finalizeBuildChain(previousGroup)
            previousGroup = group
        }

        subProjects.filterIsInstance<InstrumProject>().forEach { it.finalizeBuildChain(previousGroup) }
    }

    internal fun ecrConnection(accountId: String, awsRegion: AWSRegion, assumeRole: String = ""): ECRConnection {
        val key = nameToId(accountId, awsRegion.code.toUpperCase())

        return ecrConnections.getOrPut(key) {
            features.ecrConnection {
                id = key
                name = "Amazon ECR ($accountId ${awsRegion.code.toUpperCase()})"

                this.accountId = accountId
                this.assumeRole = assumeRole
                region = awsRegion
            }
        }
    }

    internal fun findBuild(
        idNamespace: String,
        buildName: String,
        preferredBuilds: List<InstrumBuild>
    ): InstrumBuild? {
        if (rootProject != this) {
            return rootProject.findBuild(buildName, preferredBuilds)
        }

        val matching = allProjects.flatMap { it.matchingBuilds(nameToId(idNamespace, buildName)) }
        val preferred = preferredBuilds.intersect(matching)

        if (preferred.count() == 1) {
            return preferred.first()
        }

        val count = matching.count()
        if (count > 1) {
            val namespace = if (idNamespace.isBlank()) "" else "Namespace: `$idNamespace`; "
            val message = "Found $count possibly matching builds. ${namespace}Name: `$buildName`"
            throw AmbiguousBuildReference(message)
        }

        return matching.firstOrNull()
    }

    internal fun findBuild(buildName: String, preferredBuilds: List<InstrumBuild>): InstrumBuild? =
        findBuild("", buildName, preferredBuilds)

    internal fun hasParam(name: String): Boolean = params.hasParam(name) || parent?.hasParam(name) ?: false

    internal fun matchingBuilds(buildId: String): List<InstrumBuild> =
        buildGroups.flatMap { it.matchingBuilds(buildId) }

    fun subInstrum(name: String, idNamespace: String? = null, init: InstrumProject.() -> Unit): InstrumProject =
        InstrumProject(name, idNamespace ?: name, this, init)

    fun buildGroup(parallel: Boolean = false, init: BuildGroup.() -> Unit): BuildGroup =
        BuildGroup(this, parallel, init)

    fun build(buildName: String, init: InstrumBuild.() -> Unit): InstrumBuild =
        BuildGroup(this, false).build(buildName, init)

    fun addSourceRoot(vcsUrl: String, primary: Boolean = false, init: GitVcsRoot.() -> Unit = {}): GitVcsRoot {
        val root = GitVcsRoot {
            url = vcsUrl
            authMethod = uploadedKey {
                uploadedKey = "id_rsa_teamcity"
            }

            branch = "%git.branch.default%"
            branchSpec = "%git.branch.spec%"

            useTagsAsBranches = true

            init()
        }

        if (primary || sourceRootUrl.isEmpty()) {
            sourceRootUrl = vcsUrl
            sourceVCSRoot = root
        }

        roots.add(root)
        return root
    }

    fun sourceRoot(vcsUrl: String, init: GitVcsRoot.() -> Unit = {}): GitVcsRoot {
        return addSourceRoot(vcsUrl, true) {
            id("ProjectSourceRoot")
            name = "Project Source"

            init()
        }
    }

    fun settingsRoot(vcsUrl: String, init: GitVcsRoot.() -> Unit = {}): GitVcsRoot {
        val root = GitVcsRoot {
            id("SettingsRoot")
            name = "Teamcity Versioned Settings"

            url = vcsUrl
            authMethod = uploadedKey {
                uploadedKey = "id_rsa_teamcity"
            }

            branch = "%git.branch.default%"
            branchSpec = "%git.branch.spec%"

            useTagsAsBranches = true

            init()
        }

        settingsVCSRoot = root

        roots.add(root)
        return root
    }

    fun getGlobal(setting: ProjectSetting): String? {
        return projectSettings[setting] ?: parent?.getGlobal(setting)
    }

    fun hasGlobal(setting: ProjectSetting): Boolean {
        return projectSettings.containsKey(setting) || parent?.hasGlobal(setting) ?: false
    }

    fun setGlobal(setting: ProjectSetting, value: String) {
        projectSettings[setting] = value
    }
}
