package de.hoennig.werkator.config

import com.fasterxml.jackson.annotation.JsonProperty

data class WerkatorConfig(
    /** What this file declares about the Werkator that reads it; see [VersionRequirement]. */
    val werkator: WerkatorMeta = WerkatorMeta(),
    val server: ServerConfig = ServerConfig(),
    val git: GitConfig = GitConfig(),
    val gitea: GiteaConfig = GiteaConfig(),
    val executor: ExecutorConfig = ExecutorConfig(),
    val artifacts: ArtifactsConfig = ArtifactsConfig(),
    val watcher: WatcherConfig = WatcherConfig(),
    val branches: Map<String, BranchConfig> = mapOf("default" to BranchConfig()),
    /**
     * Named build definitions (jobs) over the branches (ADR 0007), the YAML `builds`
     * section. The implicit [BuildDefinition.DEFAULT] build (`onPush` over all
     * branches) applies unless this map overrides it; see [effectiveBuildDefinitions].
     */
    @JsonProperty("builds")
    val buildDefinitions: Map<String, BuildDefinition> = emptyMap(),
) {
    /** The configured [buildDefinitions] plus the implicit `default` build unless overridden. */
    fun effectiveBuildDefinitions(): Map<String, BuildDefinition> =
        mapOf(BuildDefinition.DEFAULT to BuildDefinition(trigger = TriggerConfig(onPush = true))) + buildDefinitions

    /**
     * The settings one build of [build] on [branch] runs with: the branch entry (falling
     * back to `branches.default`) with the build definition's overrides applied last.
     * A build name without a definition — a job removed from the config since the run —
     * falls back to the plain branch settings.
     * This must stay the single answer to "what does this build run", used by the
     * executor and by everything that displays it.
     */
    fun buildSettings(
        branch: String,
        build: String,
    ): BranchConfig {
        val branchConfig = branches[branch] ?: branches["default"] ?: BranchConfig()
        val settings = effectiveBuildDefinitions()[build]?.applyTo(branchConfig) ?: branchConfig
        if (settings.docker.enabled && settings.werkdock.enabled) {
            throw IllegalArgumentException(
                "builds.$build on '$branch' enables both docker and werkdock; a build runs in exactly one sandbox. " +
                    "Disable one of them.",
            )
        }
        return settings
    }
}

data class ServerConfig(
    /**
     * Public base URL of this installation; empty defaults to `https://<nginx.serverName>/`
     * when [NginxConfig.serverName] is set (applied by [ConfigLoader]).
     */
    val publicBaseUrl: String = "",
    /** HTTP port of the `server` subcommand; 18080 like the legacy artifact server. */
    val port: Int = 18080,
    /**
     * Loopback by default: the UI and the API are unauthenticated, so reaching them should
     * require the host's reverse proxy. Set `0.0.0.0` explicitly to expose all interfaces —
     * which the managed nginx container needs (see `docs/deployment.md`).
     */
    val bindAddress: String = "127.0.0.1",
    /** Optional Impressum (legal disclosure) link shown in the web UI footer; empty hides the link. */
    val impressumUrl: String = "",
    /**
     * Resource limits for the generated systemd user unit (`init --systemd`); empty
     * means the directive is not written. Needed on platforms where the service runs
     * inside a shared memory slice, e.g. Hostsharing Managed Webspaces, where a
     * runaway Gradle build would starve everything else in the package.
     */
    val systemd: SystemdConfig = SystemdConfig(),
    val nginx: NginxConfig = NginxConfig(),
)

/** Resource-limit directives of the systemd user unit (`server.systemd`, see [ServerConfig.systemd]). */
data class SystemdConfig(
    /** `MemoryMax=` of the unit, e.g. `1G`; empty omits the directive. */
    val memoryMax: String = "",
    /** `TasksMax=` of the unit, e.g. `512`; empty omits the directive. */
    val tasksMax: String = "",
)

/**
 * Opt-in managed nginx+certbot Docker container serving Werkator over HTTPS,
 * for hosts without a usable reverse proxy (ADR 0005). Off by default; the
 * reverse-proxy deployment from `docs/deployment.md` stays the recommended setup.
 */
data class NginxConfig(
    /** Manage an nginx Docker container with Let's Encrypt certificates. */
    val enabled: Boolean = false,
    /** Public DNS name served by nginx and used for the certificate; required when [enabled]. */
    val serverName: String = "",
    /** Host port published as nginx port 80 (ACME challenge + HTTPS redirect). */
    val httpPort: Int = 8080,
    /** Host port published as nginx port 443. */
    val httpsPort: Int = 8443,
    /** Host nginx proxies to; empty uses [serverName] (the container cannot reach `localhost`). */
    val upstreamHost: String = "",
    /** Name of the managed container; empty means `werkator-nginx-<repo-name>`. */
    val containerName: String = "",
    /**
     * Directory for nginx config, certificates, and logs; empty means the platform
     * default `XDG_STATE_HOME` (or `~/.local/state`) + `/werkator/nginx/<repo-key>`.
     */
    val stateDir: String = "",
    /** E-mail for the Let's Encrypt account; empty registers without one. */
    val letsencryptEmail: String = "",
)

data class GitConfig(
    val account: String = "",
    val token: String = "",
)

data class GiteaConfig(
    val baseUrl: String = "",
    val owner: String = "",
    val repo: String = "",
    val statusContext: String = "werkator",
)

data class ArtifactsConfig(
    val retentionPerBranch: Int = 3,
    /**
     * Additionally drop builds older than this age (e.g. `30d` or `12h`); empty means no
     * age limit. Combines with [retentionPerBranch] — a build is kept only while it
     * satisfies both limits. A branch's newest build is never age-pruned, so dormant
     * branches keep their last status.
     */
    val retentionMaxAge: String = "",
    /**
     * Keep each branch's latest green (SUCCESS) build beyond [retentionPerBranch] and
     * [retentionMaxAge], so the permanent `/branches/<branch-key>/…` artifact URLs stay
     * valid while newer builds fail; the build is still dropped once its branch is gone
     * from origin.
     */
    val keepLatestGreen: Boolean = true,
    /**
     * Root directory for stored build artifacts; empty means the platform default
     * `XDG_STATE_HOME` (or `~/.local/state`) + `/werkator/artifacts/<repo-key>`.
     */
    val rootDir: String = "",
)

data class WatcherConfig(
    /** Delay between poll cycles, e.g. `10s` or `1m`. */
    val pollInterval: String = "10s",
    val newBranchMaxAge: String = "5d",
    /**
     * Honor the `branches.<name>.requirePullRequest` gates. Set false for a plain git
     * origin without pull-request refs (no Gitea/GitHub) — gated branches then build
     * on new commits like any other branch. Typically overridden per machine in
     * `.git/werkator/.werkator.yml` when the committed config enables the gates.
     */
    val pullRequestGate: Boolean = true,
    /**
     * At the end of each poll cycle, fast-forward the primary checkout's local branch
     * refs to their origin counterparts, so build tools reading the shared `.git` see
     * the same refs as origin. Fast-forward only: diverged or ahead local branches are
     * never touched. Set false to leave the local branch refs alone entirely.
     */
    val fastForwardLocalRefs: Boolean = true,
)

data class BranchConfig(
    val buildCommand: String = "./gradlew --console=plain --no-daemon test",
    val cleanCommand: String = "rm -rf build",
    val artifactDirs: List<String> = listOf("build/reports"),
    val stdoutLog: String = "build.stdout.log",
    val stderrLog: String = "build.stderr.log",
    /**
     * The watcher builds this branch only while its head commit matches a pull-request
     * head (`refs/pull/<n>/head` on origin); manual `build` commands are not affected.
     */
    val requirePullRequest: Boolean = false,
    /** Gitea commit status context of this build; empty uses `gitea.statusContext`. */
    val statusContext: String = "",
    val autoBuild: AutoBuildConfig = AutoBuildConfig(),
    val docker: DockerConfig = DockerConfig(),
    /** werkdock sandbox (bubblewrap user namespace); mutually exclusive with [docker]. */
    val werkdock: WerkdockConfig = WerkdockConfig(),
)

/**
 * The werkdock build sandbox (Step 17, executed by the werkdock CLI since step 21):
 * runs the build in an unprivileged bubblewrap user namespace over a prepared Debian
 * root filesystem. For hosts without root and without a Docker daemon (e.g. Hostsharing
 * managed webspaces); see `docs/plan/17-bwrap-build-runtime.md`.
 *
 * The section was called `bwrap` until PR#19 and is still read under that name, with a
 * warning: `bwrap` named the mechanism one layer below the tool that actually runs it,
 * which made `bwrap.werkdock` the key naming its own executor.
 */
data class WerkdockConfig(
    /** Run the clean and build commands in the sandbox instead of natively. */
    val enabled: Boolean = false,
    /**
     * Path or URL of the prepared rootfs archive (e.g. `werkator-buildenv-trixie-java21.tar.zst`),
     * unpacked on demand into `.git/werkator/buildenv/<envKey>/rootfs`; required when [enabled].
     * Pinned — a branch must not substitute a foreign rootfs via its committed config.
     */
    val rootfs: String = "",
    /**
     * The werkdock CLI executing the sandbox (step 21 session C); empty or the default
     * resolves via PATH. Pinned — a branch must not substitute the executing binary.
     * Was `bwrap.werkdock` until PR#19.
     */
    val binary: String = "werkdock",
    /** Additional environment variables set inside the sandbox. */
    val env: Map<String, String> = emptyMap(),
)

data class DockerConfig(
    /** Run the clean and build commands in a Docker container instead of natively. */
    val enabled: Boolean = false,
    /** Image for the build container; required when [enabled]. */
    val image: String = "",
    /** Dockerfile to build [image] from when it is missing or stale; empty uses [image] as-is (pulled on demand). */
    val dockerfile: String = "",
    /** Docker build context used with [dockerfile]. */
    val context: String = ".",
    /** Docker network mode for the build container; empty uses Docker's default network. */
    val network: String = "",
    /** Additional environment variables set inside the build container. */
    val env: Map<String, String> = emptyMap(),
)

/** Build execution settings, enforced by the executor for all builds regardless of their trigger. */
data class ExecutorConfig(
    /** How many builds may run at the same time; at most one build per branch runs regardless. */
    val maxConcurrent: Int = 1,
)

/**
 * Deprecated per-branch schedule (pre-ADR-0007), kept for compatibility: mapped to a
 * daily rebuild of the branch's own pool with its regular command. New configurations
 * define a build with `atTimes` in the top-level `builds` section instead.
 */
data class AutoBuildConfig(
    val enabled: Boolean = false,
    /** Daily UTC times `HH:MM`. */
    val times: List<String> = listOf("01:00"),
)
