terraform {
  required_version = ">= 1.9"

  required_providers {
    coder = {
      source  = "coder/coder"
      version = ">= 2.5"
    }
    http = {
      source  = "hashicorp/http"
      version = ">= 3.0"
    }
  }
}

variable "agent_id" {
  type        = string
  description = "The resource ID of a Coder agent."
}

variable "agent_name" {
  type        = string
  description = "The name of a Coder agent. Needed for workspaces with multiple agents."
  default     = null
}

variable "folder" {
  type        = string
  description = "The directory to open in the IDE. e.g. /home/coder/project"
  validation {
    condition     = can(regex("^(?:/[^/]+)+/?$", var.folder))
    error_message = "The folder must be a full path and must not start with a ~."
  }
}

variable "default" {
  default     = []
  type        = set(string)
  description = <<-EOT
    The default IDE selection. Removes the selection from the UI. e.g. ["CL", "GO", "IU"]
  EOT
}

variable "group" {
  type        = string
  description = "The name of a group that this app belongs to."
  default     = null
}

variable "coder_app_order" {
  type        = number
  description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
  default     = null
}

variable "coder_parameter_order" {
  type        = number
  description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
  default     = null
}

variable "tooltip" {
  type        = string
  description = "Markdown text that is displayed when hovering over workspace apps."
  default     = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
}

variable "major_version" {
  type        = string
  description = "The major version of the IDE. i.e. 2025.1"
  default     = "latest"
  validation {
    condition     = can(regex("^[0-9]{4}\\.[1-3]$", var.major_version)) || var.major_version == "latest"
    error_message = "The major_version must be a valid version number (e.g., 2025.1) or 'latest'"
  }
}

variable "channel" {
  type        = string
  description = "JetBrains IDE release channel. Valid values are release and eap."
  default     = "release"
  validation {
    condition     = can(regex("^(release|eap)$", var.channel))
    error_message = "The channel must be either release or eap."
  }
}

variable "options" {
  type        = set(string)
  description = "The list of IDE product codes."
  default     = ["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"]
  validation {
    condition = (
      alltrue([
        for code in var.options : contains(["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"], code)
      ])
    )
    error_message = "The options must be a set of valid product codes. Valid product codes are ${join(",", ["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"])}."
  }
  # check if the set is empty
  validation {
    condition     = length(var.options) > 0
    error_message = "The options must not be empty."
  }
}

variable "releases_base_link" {
  type        = string
  description = "URL of the JetBrains releases base link."
  default     = "https://data.services.jetbrains.com"
  validation {
    condition     = can(regex("^https?://.+$", var.releases_base_link))
    error_message = "The releases_base_link must be a valid HTTP/S address."
  }
}

variable "download_base_link" {
  type        = string
  description = "URL of the JetBrains download base link."
  default     = "https://download.jetbrains.com"
  validation {
    condition     = can(regex("^https?://.+$", var.download_base_link))
    error_message = "The download_base_link must be a valid HTTP/S address."
  }
}

data "http" "jetbrains_ide_versions" {
  for_each = var.ide_config == null ? local.selected_ides : toset([])
  url      = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}${var.major_version == "latest" ? "&latest=true" : ""}"
}

variable "ide_config" {
  description = <<-EOT
    Optional map of JetBrains IDE configurations keyed by product code.
    When null (default), the module fetches the latest build numbers from
    the JetBrains API at plan time. When set, all HTTP calls are skipped
    and the provided build numbers are used directly — useful for
    air-gapped environments or pinning specific versions.

    Each value must contain:
    - build: Full build number (e.g. "253.28294.337").

    Optionally override the default display name or icon:
    - name:  Display name of the IDE (e.g. "GoLand").
    - icon:  Path or URL to the IDE icon (e.g. "/icon/goland.svg").

    Example:
    {
      "GO" = { build = "261.22158.291" },
      "IU" = { build = "261.22158.277" },
    }
  EOT
  type = map(object({
    build = string
    name  = optional(string)
    icon  = optional(string)
  }))
  default = null
  validation {
    condition     = var.ide_config == null || length(var.ide_config) > 0
    error_message = "The ide_config must not be empty."
  }
  # ide_config must be a superset of var.options
  # Requires Terraform 1.9+ for cross-variable validation references
  validation {
    condition = var.ide_config == null || alltrue([
      for code in var.options : contains(keys(var.ide_config), code)
    ])
    error_message = "The ide_config must contain entries for all IDE codes in var.options. Either add the missing entries to ide_config or narrow var.options to match."
  }
  # ide_config must also cover all codes in var.default to avoid
  # key-not-found errors when building options_metadata.
  validation {
    condition = var.ide_config == null || alltrue([
      for code in var.default : contains(keys(var.ide_config), code)
    ])
    error_message = "The ide_config must contain entries for all IDE codes in var.default."
  }
  # major_version, channel, and releases_base_link only affect the
  # HTTP call, which is skipped when ide_config is set. Reject
  # non-default values to avoid silently ignoring user intent.
  validation {
    condition = var.ide_config == null || (
      var.major_version == "latest" &&
      var.channel == "release" &&
      var.releases_base_link == "https://data.services.jetbrains.com"
    )
    error_message = "major_version, channel, and releases_base_link have no effect when ide_config is set. Remove them or unset ide_config."
  }
}

locals {
  # Static IDE metadata for name and icon lookups when ide_config is null.
  ide_metadata = {
    "CL" = { name = "CLion", icon = "/icon/clion.svg" }
    "GO" = { name = "GoLand", icon = "/icon/goland.svg" }
    "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg" }
    "PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg" }
    "PY" = { name = "PyCharm", icon = "/icon/pycharm.svg" }
    "RD" = { name = "Rider", icon = "/icon/rider.svg" }
    "RM" = { name = "RubyMine", icon = "/icon/rubymine.svg" }
    "RR" = { name = "RustRover", icon = "/icon/rustrover.svg" }
    "WS" = { name = "WebStorm", icon = "/icon/webstorm.svg" }
  }

  # Determine the user's actual IDE selection.
  # This is computed before the HTTP data source so that version lookups
  # are only performed for IDEs the user chose — not every option.
  selected_ides = length(var.default) == 0 ? toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ides[0].value, "[]"))) : toset(var.default)

  # Parse HTTP responses. Only populated when ide_config is null
  # and the module fetches versions from the JetBrains API.
  # No try() fallback — if the API is expected and fails, Terraform
  # should error rather than silently using stale build numbers.
  parsed_responses = {
    for code, response in data.http.jetbrains_ide_versions :
    code => jsondecode(response.response_body)
  }

  # Filter the parsed response for the requested major version if not "latest"
  filtered_releases = {
    for code, parsed in local.parsed_responses : code => [
      for r in parsed[keys(parsed)[0]] :
      r if var.major_version == "latest" || r.majorVersion == var.major_version
    ]
  }

  # Select the latest release for the requested major version (first item in the filtered list)
  selected_releases = {
    for code, releases in local.filtered_releases :
    code => length(releases) > 0 ? releases[0] : null
  }

  # Dynamically generate IDE configurations based on selected IDEs
  options_metadata = {
    for code in local.selected_ides : code => {
      icon       = var.ide_config != null ? coalesce(var.ide_config[code].icon, local.ide_metadata[code].icon) : local.ide_metadata[code].icon
      name       = var.ide_config != null ? coalesce(var.ide_config[code].name, local.ide_metadata[code].name) : local.ide_metadata[code].name
      identifier = code
      key        = code

      # When ide_config is set, use the pinned build number directly.
      # When fetching from API, use the API result (fails if unavailable).
      build = var.ide_config != null ? var.ide_config[code].build : local.selected_releases[code].build

      # API response data, null when using ide_config.
      json_data = var.ide_config != null ? null : local.selected_releases[code]
    }
  }
}

data "coder_parameter" "jetbrains_ides" {
  count        = length(var.default) == 0 ? 1 : 0
  type         = "list(string)"
  name         = "jetbrains_ides"
  description  = "Select which JetBrains IDEs to configure for use in this workspace."
  display_name = "JetBrains IDEs"
  icon         = "/icon/jetbrains-toolbox.svg"
  mutable      = true
  default      = jsonencode([])
  order        = var.coder_parameter_order
  form_type    = "multi-select" # requires Coder version 2.24+

  dynamic "option" {
    for_each = var.options
    content {
      icon  = var.ide_config != null ? coalesce(var.ide_config[option.value].icon, local.ide_metadata[option.value].icon) : local.ide_metadata[option.value].icon
      name  = var.ide_config != null ? coalesce(var.ide_config[option.value].name, local.ide_metadata[option.value].name) : local.ide_metadata[option.value].name
      value = option.value
    }
  }
}

data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}

resource "coder_app" "jetbrains" {
  for_each     = local.selected_ides
  agent_id     = var.agent_id
  slug         = "jetbrains-${lower(each.key)}"
  display_name = local.options_metadata[each.key].name
  icon         = local.options_metadata[each.key].icon
  external     = true
  order        = var.coder_app_order
  group        = var.group
  tooltip      = var.tooltip
  url = join("", [
    "jetbrains://gateway/coder?&workspace=", # requires 2.6.3+ version of Toolbox
    data.coder_workspace.me.name,
    "&owner=",
    data.coder_workspace_owner.me.name,
    "&folder=",
    var.folder,
    "&url=",
    data.coder_workspace.me.access_url,
    "&token=",
    "$SESSION_TOKEN",
    "&ide_product_code=",
    each.key,
    "&ide_build_number=",
    local.options_metadata[each.key].build,
    var.agent_name != null ? "&agent_name=${var.agent_name}" : "",
  ])
}

output "ide_metadata" {
  description = "A map of the metadata for each selected JetBrains IDE."
  value = {
    # We iterate directly over the selected_ides map.
    # 'key' will be the IDE key (e.g., "IC", "PY")
    for key, val in local.selected_ides : key => local.options_metadata[key]
  }
}
