{ config, lib, pkgs, ... }:

let
  cfg = config.services.deployer;
  tomlFormat = pkgs.formats.toml { };
  usesLocalChainfire =
    builtins.any
      (
        endpoint:
        lib.hasPrefix "http://127.0.0.1:" endpoint
        || lib.hasPrefix "http://localhost:" endpoint
        || lib.hasPrefix "http://[::1]:" endpoint
      )
      cfg.chainfireEndpoints;
  localChainfireDeps =
    lib.optionals
      (
        usesLocalChainfire
        && lib.hasAttrByPath [ "services" "chainfire" "enable" ] config
        && config.services.chainfire.enable
      )
      [ "chainfire.service" ];
  generatedConfig = {
    bind_addr = cfg.bindAddr;
    chainfire = {
      endpoints = cfg.chainfireEndpoints;
      namespace = cfg.chainfireNamespace;
    };
    cluster_namespace = cfg.clusterNamespace;
    heartbeat_timeout_secs = cfg.heartbeatTimeoutSecs;
    local_state_path = cfg.localStatePath;
    allow_admin_fallback = cfg.allowAdminFallback;
    allow_unauthenticated = cfg.allowUnauthenticated;
    require_chainfire = cfg.requireChainfire;
    allow_unknown_nodes = cfg.allowUnknownNodes;
    allow_test_mappings = cfg.allowTestMappings;
    tls_self_signed = cfg.tlsSelfSigned;
  }
  // lib.optionalAttrs (cfg.clusterId != null) {
    cluster_id = cfg.clusterId;
  }
  // lib.optionalAttrs (cfg.bootstrapFlakeBundle != null) {
    bootstrap_flake_bundle_path = toString cfg.bootstrapFlakeBundle;
  }
  // lib.optionalAttrs (cfg.tlsCaCertPath != null) {
    tls_ca_cert_path = cfg.tlsCaCertPath;
  }
  // lib.optionalAttrs (cfg.tlsCaKeyPath != null) {
    tls_ca_key_path = cfg.tlsCaKeyPath;
  };
  configFile = tomlFormat.generate "deployer.toml" generatedConfig;
in
{
  options.services.deployer = {
    enable = lib.mkEnableOption "deployer bootstrap orchestration service";

    bindAddr = lib.mkOption {
      type = lib.types.str;
      default = "0.0.0.0:8080";
      description = "Bind address for deployer HTTP API";
    };

    chainfireEndpoints = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      default = [ ];
      description = "ChainFire endpoints for persistent deployer state";
      example = [ "http://127.0.0.1:2379" ];
    };

    chainfireNamespace = lib.mkOption {
      type = lib.types.str;
      default = "deployer";
      description = "Namespace prefix used in ChainFire";
    };

    clusterId = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      default = null;
      description = "Cluster ID used when writing desired state";
    };

    clusterNamespace = lib.mkOption {
      type = lib.types.str;
      default = "photoncloud";
      description = "Cluster namespace prefix";
    };

    heartbeatTimeoutSecs = lib.mkOption {
      type = lib.types.int;
      default = 300;
      description = "Node heartbeat timeout in seconds";
    };

    localStatePath = lib.mkOption {
      type = lib.types.str;
      default = "/var/lib/deployer/state";
      description = "Local storage path for deployer bootstrap state";
    };

    bootstrapFlakeBundle = lib.mkOption {
      type = lib.types.nullOr lib.types.path;
      default = null;
      description = "Optional tar.gz bundle served to bootstrap installers as the canonical PhotonCloud flake source";
    };

    requireChainfire = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = "Fail startup when ChainFire is unavailable";
    };

    allowUnauthenticated = lib.mkOption {
      type = lib.types.bool;
      default = true;
      description = "Allow unauthenticated API requests";
    };

    allowUnknownNodes = lib.mkOption {
      type = lib.types.bool;
      default = true;
      description = "Allow unknown machine-id auto registration";
    };

    allowTestMappings = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = "Enable built-in test machine-id mappings";
    };

    bootstrapToken = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      default = null;
      description = "Shared bootstrap token for phone-home API";
    };

    adminToken = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      default = null;
      description = "Shared admin token for admin APIs";
    };

    clusterStateFile = lib.mkOption {
      type = lib.types.nullOr lib.types.path;
      default = null;
      description = "Optional declarative cluster state JSON/YAML file applied with deployer-ctl";
    };

    seedClusterState = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = "Apply the declarative cluster state file to ChainFire during boot";
    };

    seedClusterStatePrune = lib.mkOption {
      type = lib.types.bool;
      default = true;
      description = "Prune stale cluster-state objects when applying declarative state";
    };

    seedClusterStateRetryAttempts = lib.mkOption {
      type = lib.types.int;
      default = 30;
      description = "Number of retries when seeding declarative cluster state";
    };

    seedClusterStateRetrySecs = lib.mkOption {
      type = lib.types.int;
      default = 5;
      description = "Seconds to wait between deployer cluster-state seed retries";
    };

    allowAdminFallback = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = "Allow admin auth fallback to bootstrap token";
    };

    tlsCaCertPath = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      default = null;
      description = "Optional CA certificate path for issuing node TLS certificates";
    };

    tlsCaKeyPath = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      default = null;
      description = "Optional CA private key path for issuing node TLS certificates";
    };

    tlsSelfSigned = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = "Issue self-signed node certificates when CA is not configured";
    };

    package = lib.mkOption {
      type = lib.types.package;
      default = pkgs.deployer-server or (throw "deployer-server package not found");
      description = "Package to use for deployer";
    };

    ctlPackage = lib.mkOption {
      type = lib.types.package;
      default = pkgs.deployer-ctl or (throw "deployer-ctl package not found");
      description = "Package to use for deployer-ctl";
    };
  };

  config = lib.mkIf cfg.enable {
    assertions = [
      {
        assertion = (!cfg.seedClusterState) || cfg.clusterStateFile != null;
        message = "services.deployer.seedClusterState requires services.deployer.clusterStateFile";
      }
      {
        assertion = (!cfg.seedClusterState) || cfg.chainfireEndpoints != [ ];
        message = "services.deployer.seedClusterState requires services.deployer.chainfireEndpoints";
      }
    ];

    services.deployer.clusterStateFile =
      lib.mkDefault (
        if config.system.build ? plasmacloudDeployerClusterState then
          config.system.build.plasmacloudDeployerClusterState
        else
          null
      );

    users.users.deployer = {
      isSystemUser = true;
      group = "deployer";
      description = "Deployer service user";
      home = "/var/lib/deployer";
    };

    users.groups.deployer = { };

    systemd.tmpfiles.rules = [
      "d /var/lib/deployer 0750 deployer deployer -"
      "d ${cfg.localStatePath} 0750 deployer deployer -"
    ];

    systemd.services.deployer = {
      description = "PlasmaCloud Deployer Server";
      wantedBy = [ "multi-user.target" ];
      wants = [ "network-online.target" ] ++ localChainfireDeps;
      after = [ "network-online.target" ] ++ localChainfireDeps;
      requires = localChainfireDeps;

      environment = {}
        // lib.optionalAttrs (cfg.bootstrapToken != null) {
          DEPLOYER_BOOTSTRAP_TOKEN = cfg.bootstrapToken;
        }
        // lib.optionalAttrs (cfg.adminToken != null) {
          DEPLOYER_ADMIN_TOKEN = cfg.adminToken;
        };

      serviceConfig = {
        Type = "simple";
        User = "deployer";
        Group = "deployer";
        Restart = "on-failure";
        RestartSec = "5s";

        StateDirectory = "deployer";
        StateDirectoryMode = "0750";

        NoNewPrivileges = true;
        PrivateTmp = true;
        ProtectSystem = "strict";
        ProtectHome = true;
        ReadWritePaths = [ "/var/lib/deployer" cfg.localStatePath ];

        ExecStart = "${cfg.package}/bin/deployer-server --config ${configFile}";
      };
    };

    systemd.services.deployer-seed-cluster-state = lib.mkIf cfg.seedClusterState {
      description = "Seed PlasmaCloud cluster state from declarative Nix output";
      wantedBy = [ "multi-user.target" ];
      wants = [ "network-online.target" "deployer.service" ];
      after = [ "network-online.target" "deployer.service" ];
      path = [ pkgs.coreutils cfg.ctlPackage ];

      script =
        let
          chainfireEndpointsArg = lib.concatStringsSep "," cfg.chainfireEndpoints;
          pruneArg = lib.optionalString cfg.seedClusterStatePrune " --prune";
        in
        ''
          set -euo pipefail

          cluster_id_args=()
          ${lib.optionalString (cfg.clusterId != null) ''
            cluster_id_args+=(--cluster-id ${lib.escapeShellArg cfg.clusterId})
          ''}

          attempt=1
          until [ "$attempt" -gt ${toString cfg.seedClusterStateRetryAttempts} ]; do
            if ${cfg.ctlPackage}/bin/deployer-ctl \
              --chainfire-endpoint ${lib.escapeShellArg chainfireEndpointsArg} \
              "''${cluster_id_args[@]}" \
              --cluster-namespace ${lib.escapeShellArg cfg.clusterNamespace} \
              --deployer-namespace ${lib.escapeShellArg cfg.chainfireNamespace} \
              apply --config ${lib.escapeShellArg (toString cfg.clusterStateFile)}${pruneArg}; then
              exit 0
            fi

            echo "deployer cluster-state seed attempt $attempt/${toString cfg.seedClusterStateRetryAttempts} failed; retrying in ${toString cfg.seedClusterStateRetrySecs}s" >&2
            attempt=$((attempt + 1))
            sleep ${toString cfg.seedClusterStateRetrySecs}
          done

          echo "failed to seed deployer cluster state after ${toString cfg.seedClusterStateRetryAttempts} attempts" >&2
          exit 1
        '';

      serviceConfig = {
        Type = "oneshot";
        User = "deployer";
        Group = "deployer";
        NoNewPrivileges = true;
        PrivateTmp = true;
        ProtectSystem = "strict";
        ProtectHome = true;
      };
    };
  };
}
