AWSTemplateFormatVersion: '2010-09-09'
Description: >-
  Blacktip Refine — Multi-Cloud Cost Intelligence (AWS Marketplace).
  Provisions a self-hosted Refine deployment in your own AWS account: EC2
  with Docker, an S3 bucket, IAM role, security group(s), optional
  Elastic IP, and an optional HTTPS Application Load Balancer. Licensing
  follows the stack's fulfillment mode: Contract deployments verify your
  AWS Marketplace entitlement automatically (no manual steps — open
  RefineUrl from the Outputs once the stack completes); BYOL deployments
  receive an Ed25519-signed license from Blacktip Solutions via the S3
  bucket (see the NextStep output for instructions).

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: 'Server Setup'
        Parameters:
          - InstanceType
          - KeyPairName
          - RootVolumeSize
          - EnableElasticIp
          - AutoAssignPublicIp
      - Label:
          default: 'Operating System'
        Parameters:
          - OperatingSystem
          - CustomAmiId
      - Label:
          default: 'Network'
        Parameters:
          - VpcId
          - SubnetId
          - AllowedSshCidr
          - AllowedAppCidr
      - Label:
          default: 'HTTPS / Load Balancer (highly recommended for production)'
        Parameters:
          - EnableHttps
          - AcmCertificateArn
          - LoadBalancerSubnetIds
          - LoadBalancerScheme
      - Label:
          default: 'Bring Your Own (Advanced — leave defaults for typical deployments)'
        Parameters:
          - CreateInstanceProfile
          - ExistingInstanceProfileArn
          - CreateEc2SecurityGroup
          - ExistingEc2SecurityGroupId
          - CreateAlbSecurityGroup
          - ExistingAlbSecurityGroupId
          - CreateLoadBalancer
          - ExistingLoadBalancerArn
          - ExistingLoadBalancerListenerArn
          - ExistingLoadBalancerHostHeader
          - ExistingLoadBalancerListenerRulePriority
      - Label:
          default: 'Delivery Package'
        Parameters:
          - UseExistingBucket
          - ExistingBucketName
          - ExistingObjectKey
      - Label:
          default: 'Tagging'
        Parameters:
          - EnvironmentTag
          - CustomerName
      - Label:
          default: 'Marketplace (advanced — leave defaults)'
        Parameters:
          - MarketplaceFulfillmentMode
          - RefineImageUri
      - Label:
          default: 'Auto-update'
        Parameters:
          - EnableAutoUpdate
          - AutoUpdateSchedule
          - AutoUpdateNotifyEmail
          - EnableRollbackOnHealthFailure
    ParameterLabels:
      MarketplaceFulfillmentMode:
        default: 'Marketplace fulfillment mode'
      RefineImageUri:
        default: 'Refine container image URI'
      EnableAutoUpdate:
        default: 'Enable automatic updates'
      AutoUpdateSchedule:
        default: 'Auto-update schedule (cron, UTC)'
      AutoUpdateNotifyEmail:
        default: 'Notification email for updates (optional)'
      EnableRollbackOnHealthFailure:
        default: 'Roll back failed updates automatically'
      InstanceType:
        default: 'Instance type'
      KeyPairName:
        default: 'SSH key pair'
      RootVolumeSize:
        default: 'Root volume size (GB)'
      EnableElasticIp:
        default: 'Attach an Elastic IP'
      OperatingSystem:
        default: 'Operating system'
      CustomAmiId:
        default: 'Custom AMI ID (only if OS = Custom)'
      VpcId:
        default: 'VPC'
      SubnetId:
        default: 'Subnet (must be public)'
      AllowedSshCidr:
        default: 'Allowed CIDR for SSH'
      AllowedAppCidr:
        default: 'Allowed CIDR for Refine UI'
      EnableHttps:
        default: 'Enable HTTPS via Application Load Balancer (highly recommended)'
      AcmCertificateArn:
        default: 'ACM certificate ARN (required if HTTPS enabled)'
      LoadBalancerSubnetIds:
        default: 'Subnets for ALB (≥ 2 in different AZs, required if HTTPS enabled)'
      UseExistingBucket:
        default: 'Use an existing S3 bucket?'
      ExistingBucketName:
        default: 'Existing bucket name'
      ExistingObjectKey:
        default: 'Existing object key'
      EnvironmentTag:
        default: 'Environment'
      CustomerName:
        default: 'Customer name (optional)'

Parameters:
  # ── Server Setup ──
  InstanceType:
    Type: String
    Default: t3.medium
    AllowedValues:
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - m6i.large
      - m6i.xlarge
      - m6i.2xlarge
    Description: 'EC2 instance type. t3.medium is the recommended minimum (1-10 AWS accounts). m6i.xlarge for 50+. t3.micro and t3.small are SMOKE TEST ONLY (Free Tier eligible) - Refine may OOM with 1-2 GB RAM under any real load.'

  KeyPairName:
    Type: String
    Default: ''
    Description: 'Optional: existing EC2 key pair name for SSH access. Leave BLANK to use AWS Systems Manager Session Manager instead (no SSH key needed — connect via AWS console "Connect" button).'

  RootVolumeSize:
    Type: Number
    Default: 50
    MinValue: 30
    MaxValue: 500
    Description: 'Root EBS volume size in GB'

  EnableElasticIp:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: 'Attach an Elastic IP so the public IP is stable across restarts'

  # ── Operating System ──
  OperatingSystem:
    Type: String
    Default: Ubuntu2204
    AllowedValues:
      - Ubuntu2204
      - AmazonLinux2023
      - Custom
    Description: 'Operating system. Custom requires you to provide a Debian/Ubuntu/Amazon-Linux derivative AMI.'

  CustomAmiId:
    Type: String
    Default: ''
    Description: 'AMI ID (e.g., ami-0123456789abcdef0). Only used if OperatingSystem = Custom.'

  # ── Network ──
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: 'VPC where the server will be launched'

  SubnetId:
    Type: AWS::EC2::Subnet::Id
    Description: 'Public subnet (must have route to internet for Docker image pull)'

  AllowedSshCidr:
    Type: String
    Default: ''
    Description: 'Only used if KeyPairName is set. CIDR block allowed to SSH (e.g., 203.0.113.0/32 for your office IP). Find your IP at https://checkip.amazonaws.com — append /32. Leave blank if using SSM Session Manager.'

  AllowedAppCidr:
    Type: String
    Description: 'CIDR allowed to access the Refine UI. If EnableHttps=true, this is the inbound CIDR for the ALB on port 443 (0.0.0.0/0 is OK because HTTPS protects in transit). If EnableHttps=false, this is the inbound CIDR for the EC2 on plaintext port 8000 — must be restricted to your office/VPN CIDR (0.0.0.0/0 is BLOCKED).'
    AllowedPattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'

  # ── HTTPS / Load Balancer ──
  EnableHttps:
    Type: String
    Default: 'false'
    AllowedValues: ['true', 'false']
    Description: 'Default false so the stack launches with no extra prerequisites (recommended for a first launch / self-service subscribe). If true, an Application Load Balancer with HTTPS:443 is created in front of the EC2 (REQUIRES AcmCertificateArn AND at least 2 LoadBalancerSubnetIds in different Availability Zones — ALBs are multi-AZ). Enable HTTPS once you have an ACM certificate; until then restrict AllowedAppCidr to trusted networks since the EC2 serves plaintext HTTP:8000.'

  AcmCertificateArn:
    Type: String
    Default: ''
    Description: 'Required if EnableHttps=true. ARN of your existing ACM certificate (must be in the same region as this stack). Create one in advance via the ACM console — DNS validation typically takes 5-10 min. Format: arn:aws:acm:REGION:ACCOUNT:certificate/UUID.'

  LoadBalancerSubnetIds:
    Type: CommaDelimitedList
    Default: ''
    Description: 'Required if EnableHttps=true. At least TWO subnet IDs in DIFFERENT AZs (ALBs require multi-AZ). E.g., subnet-aaa,subnet-bbb. If LoadBalancerScheme=internet-facing, use PUBLIC subnets (need IGW route). If LoadBalancerScheme=internal, use PRIVATE subnets (reachable from your VPN/Direct Connect/peered VPCs). Leave blank if EnableHttps=false.'

  LoadBalancerScheme:
    Type: String
    Default: internet-facing
    AllowedValues: [internet-facing, internal]
    Description: 'internet-facing (default): ALB has a public DNS reachable from the internet (restrict via AllowedAppCidr). internal: ALB has only a private DNS reachable from the VPC, peered VPCs, or VPN/Direct Connect clients. Required for gov/SCP-restricted deployments where the server must NOT be publicly reachable.'

  AutoAssignPublicIp:
    Type: String
    Default: subnet-default
    AllowedValues: [subnet-default, 'true', 'false']
    Description: 'Whether to auto-assign a public IPv4 to the EC2. subnet-default (default): inherit from the subnet (no replacement on stack updates for existing customers). true: force public IP. false: no public IP (private-subnet deployment). When false, the EC2 still needs egress to AWS APIs and the public internet (Docker, GitHub) via a NAT Gateway or VPC endpoints — see NETWORK_ARCHITECTURE.md.'

  # ── Delivery Package ──
  UseExistingBucket:
    Type: String
    Default: 'false'
    AllowedValues: ['true', 'false']
    Description: 'If false (default), the stack creates a new S3 bucket. Blacktip Solutions uploads ONLY your Ed25519-signed license file to it as "refine.license" (the Docker image pulls directly from public ECR, so no image bundle is needed).'

  ExistingBucketName:
    Type: String
    Default: ''
    Description: 'Only used if UseExistingBucket = true. Name of your existing S3 bucket.'

  ExistingObjectKey:
    Type: String
    Default: 'refine.license'
    Description: 'Only used if UseExistingBucket = true. Object key of the license file.'

  # ── Bring Your Own (Advanced) ──
  # Each toggle defaults to 'true' (auto-create), so existing customers see no
  # behavior change. Set to 'false' + provide the existing resource id/arn to
  # let CF use a pre-existing corporate resource instead of creating a new one.
  CreateInstanceProfile:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: 'true (default): CF creates an IAM role + instance profile for the EC2. false: bring your own (provide ExistingInstanceProfileArn).'

  ExistingInstanceProfileArn:
    Type: String
    Default: ''
    Description: 'Required if CreateInstanceProfile=false. ARN of an existing IAM instance profile. Must include the AmazonSSMManagedInstanceCore managed policy and read access to your delivery S3 bucket.'

  CreateEc2SecurityGroup:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: 'true (default): CF creates the EC2 security group. false: bring your own (provide ExistingEc2SecurityGroupId). The existing SG must allow inbound TCP 8000 from the ALB SG (HTTPS mode) or AllowedAppCidr (HTTP mode).'

  ExistingEc2SecurityGroupId:
    Type: String
    Default: ''
    Description: 'Required if CreateEc2SecurityGroup=false. SG ID of your existing EC2 security group.'

  CreateAlbSecurityGroup:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: 'true (default): CF creates the ALB security group. false: bring your own (provide ExistingAlbSecurityGroupId). Only valid when CreateLoadBalancer=false.'

  ExistingAlbSecurityGroupId:
    Type: String
    Default: ''
    Description: 'Required if CreateAlbSecurityGroup=false. SG ID of your existing ALB security group. The EC2 SG ingress on port 8000 will reference this.'

  CreateLoadBalancer:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: 'true (default): CF creates an ALB. false: bring your own corporate ALB (provide ExistingLoadBalancerArn + ExistingLoadBalancerListenerArn). CF will add a listener rule to your existing HTTPS listener that routes ExistingLoadBalancerHostHeader to a new target group containing the Refine EC2.'

  ExistingLoadBalancerArn:
    Type: String
    Default: ''
    Description: 'Required if CreateLoadBalancer=false. ARN of your existing Application Load Balancer.'

  ExistingLoadBalancerListenerArn:
    Type: String
    Default: ''
    Description: 'Required if CreateLoadBalancer=false. ARN of your existing HTTPS:443 listener. CF will add a listener rule to this listener.'

  ExistingLoadBalancerHostHeader:
    Type: String
    Default: ''
    Description: 'Required if CreateLoadBalancer=false. Host header that routes to Refine (e.g., refine.yourcompany.com). The new listener rule matches on this host header.'

  ExistingLoadBalancerListenerRulePriority:
    Type: Number
    Default: 100
    MinValue: 1
    MaxValue: 50000
    Description: 'Only used if CreateLoadBalancer=false. Priority of the listener rule CF adds to your existing listener (1=highest, 50000=lowest). Pick a value not used by other rules on the same listener.'

  # ── Tagging ──
  EnvironmentTag:
    Type: String
    Default: production
    AllowedValues: [production, staging, dev, test]
    Description: 'Environment tag applied to all resources for cost allocation.'

  CustomerName:
    Type: String
    Default: ''
    Description: 'Optional: your internal customer/team name (e.g., "InsequenceINC"). Tagged on all resources.'

  # ── Marketplace (do not change unless instructed by Blacktip Solutions) ──
  MarketplaceFulfillmentMode:
    Type: String
    Default: 'Contract'
    AllowedValues: ['BYOL', 'Contract']
    Description: 'BYOL (default): the subscription is free and Blacktip emails your Ed25519-signed license out-of-band after you submit the request form (used for the Limited listing, direct sales, and GovCloud). Contract: the app verifies your AWS Marketplace entitlement directly (in-Marketplace fulfillment) - this is the path for the PUBLIC listing. Leave as BYOL until the entitlement flow is validated end-to-end against a real subscription.'

  RefineMpProductSku:
    Type: String
    Default: 'prod-bjhozrcxvsmtm'
    Description: 'Contract mode only (do not change): the contract product ID used for License Manager CheckoutLicense entitlement verification. Ignored in BYOL mode.'

  RefineMpEntitlements:
    Type: String
    Default: 'Starter:None,Core:None,Growth:None,Scale:None,Enterprise:None'
    Description: 'Contract mode only (do not change): comma-separated Name:Unit entitlement dimensions checked via CheckoutLicense. Must match the listing''s contract pricing dimension API names. Ignored in BYOL mode.'

  RefineImageUri:
    Type: String
    Default: 'public.ecr.aws/a4k0g9y9/refine:latest'
    Description: 'Refine container image. Default points at the :latest tag on the official Blacktip ECR Public image — pair with EnableAutoUpdate=true for automatic updates. To pin to a specific version, set to public.ecr.aws/a4k0g9y9/refine:vX.Y.Z (and Watchtower will not update it because the digest never changes for a pinned tag). Once the blacktip custom alias is approved by AWS, swap a4k0g9y9 for blacktip — both URLs resolve to the same image.'

  BootstrapUrl:
    Type: String
    Default: 'https://blacktip-marketplace-templates.s3.amazonaws.com/refine/v3.9.2/bootstrap.sh'
    Description: 'URL of the externalized bootstrap script. The instance boot script (UserData) is capped at 16 KB by EC2, so the full host setup lives here and UserData just downloads + runs it. Pinned per release alongside the template; override only for a custom/air-gapped bootstrap.'

  AdminEmail:
    Type: String
    Default: 'admin@refine.local'
    Description: 'Email/username for the Refine administrator account that is created automatically on first launch. This is what you log in with. Use any address you like (it does not need to receive mail).'

  AdminPassword:
    Type: String
    NoEcho: true
    Default: ''
    Description: 'Password for the administrator account created on first launch. SET A STRONG VALUE so you can log in immediately. If left blank, a random password is generated and you must reset it from the host via SSM. Stored only in your own instance .env (never transmitted).'

  StartInDemoMode:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: 'When true (default), Refine boots into a read-only DEMO mode with synthetic data BEFORE your license is installed, so you can explore the product immediately instead of waiting at a license screen. Demo mode connects no real accounts and changes nothing. The moment your real license is delivered to the bucket, Refine automatically switches to live mode and clears the demo data. Set false to require your license before the app serves.'

  EnableAutoUpdate:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: 'When true, deploys a Watchtower sidecar container that periodically polls the Refine image tag in AWS Public ECR and rolls the Refine container when a new image is available. Recommended ON for self-managed deployments — Blacktip ships frequent improvements. Set to false to pin your stack to whatever RefineImageUri tag you launched with (you can still update manually via SSM or by updating this stack with a new RefineImageUri).'

  AutoUpdateSchedule:
    Type: String
    Default: '0 0 3 * * *'
    Description: 'Cron schedule (6-field with seconds, UTC) for Watchtower to check for new Refine images. Default 0 0 3 * * * = every day at 03:00:00 UTC. Pick a window that overlaps your lowest-traffic time. Updates roll the Refine container with ~30 seconds of unavailability; if you have multiple Refine users, schedule during off-hours. Ignored if EnableAutoUpdate=false.'

  AutoUpdateNotifyEmail:
    Type: String
    Default: ''
    Description: 'Optional. Email address that receives a notification from Watchtower whenever the Refine container is updated (or fails to update). Requires the EC2 host to be able to send mail via Postfix/SMTP or a relay you configure. Leave blank to disable notifications — most customers monitor the Refine UI directly.'

  EnableRollbackOnHealthFailure:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: 'When true, deploys a host-side monitor that tracks the digest of the last-known-healthy Refine image and automatically rolls back the container if a new image stays unhealthy (returns non-200 from /health) for more than 5 minutes. The monitor sleeps for 1 hour after a rollback to avoid loops, and only triggers once before requiring operator intervention. Recommended ON when EnableAutoUpdate is also ON. Has no effect if EnableAutoUpdate=false.'

Conditions:
  # Fulfillment-mode gates: BYOL-only outputs (license form, delivery bucket
  # instructions) must not appear on Contract-mode stacks — the public listing
  # was audit-rejected precisely for out-of-Marketplace fulfillment surface.
  IsByolFulfillment: !Equals [!Ref MarketplaceFulfillmentMode, 'BYOL']
  IsContractFulfillment: !Equals [!Ref MarketplaceFulfillmentMode, 'Contract']
  CreateNewBucket: !Equals [!Ref UseExistingBucket, 'false']
  UseUbuntu: !Equals [!Ref OperatingSystem, 'Ubuntu2204']
  UseAmazonLinux: !Equals [!Ref OperatingSystem, 'AmazonLinux2023']
  UseCustomAmi: !Equals [!Ref OperatingSystem, 'Custom']
  HasEip: !Equals [!Ref EnableElasticIp, 'true']
  HasKeyPair: !Not [!Equals [!Ref KeyPairName, '']]
  HasSshCidr: !Not [!Equals [!Ref AllowedSshCidr, '']]
  EnableSshIngress: !And
    - !Condition HasKeyPair
    - !Condition HasSshCidr
  HttpsEnabled: !Equals [!Ref EnableHttps, 'true']
  HttpMode: !Equals [!Ref EnableHttps, 'false']
  # When AutoAssignPublicIp != subnet-default, the EC2 needs a NetworkInterfaces
  # block to set AssociatePublicIpAddress explicitly. Default keeps the simple
  # SubnetId/SecurityGroupIds form so existing stacks update without replacement.
  UseExplicitPublicIp: !Not [!Equals [!Ref AutoAssignPublicIp, 'subnet-default']]
  ForcePublicIp: !Equals [!Ref AutoAssignPublicIp, 'true']
  # Internal-ALB mode: surfaced in outputs so we use the ALB's private DNS instead
  # of the EC2's public IP. Also informs CF Rules about acceptable AllowedAppCidr.
  IsInternalAlb: !And
    - !Condition HttpsEnabled
    - !Equals [!Ref LoadBalancerScheme, 'internal']

  # ── Bring-Your-Own toggles ──
  CreateNewInstanceProfile: !Equals [!Ref CreateInstanceProfile, 'true']
  CreateNewEc2Sg:           !Equals [!Ref CreateEc2SecurityGroup, 'true']
  CreateNewAlb:             !And
    - !Condition HttpsEnabled
    - !Equals [!Ref CreateLoadBalancer, 'true']
  UseExistingAlb:           !And
    - !Condition HttpsEnabled
    - !Equals [!Ref CreateLoadBalancer, 'false']
  CreateNewAlbSg:           !And
    - !Condition CreateNewAlb
    - !Equals [!Ref CreateAlbSecurityGroup, 'true']
  # Compound: HTTP-mode ingress + we own the EC2 SG (don't add ingress to a BYO SG)
  AddHttpIngress: !And
    - !Condition HttpMode
    - !Condition CreateNewEc2Sg
  # Compound: HTTPS-mode ingress to EC2 SG from ALB SG (only when we own the EC2 SG)
  AddAlbIngress: !And
    - !Condition HttpsEnabled
    - !Condition CreateNewEc2Sg
  # Compound: SSH ingress only when we own the EC2 SG and the user opted in
  AddSshIngress: !And
    - !Condition EnableSshIngress
    - !Condition CreateNewEc2Sg

# CloudFormation Rules — validate parameters at stack creation time.
# These are fail-fast guardrails that prevent insecure configurations.
Rules:
  # Block AllowedAppCidr=0.0.0.0/0 ONLY when HTTPS is disabled. In HTTPS mode,
  # 0.0.0.0/0 on the ALB is a normal public-SaaS configuration because traffic
  # is encrypted in transit. In HTTP mode, plaintext exposure to the world
  # would leak LDAP passwords and session cookies via MITM.
  BlockPublicAppCidrInHttpMode:
    RuleCondition: !And
      - !Equals [!Ref EnableHttps, 'false']
      - !Equals [!Ref AllowedAppCidr, '0.0.0.0/0']
    Assertions:
      - Assert: !Equals ['false', 'true']
        AssertDescription: >-
          AllowedAppCidr=0.0.0.0/0 is blocked when EnableHttps=false. Plaintext HTTP
          exposed to the entire internet would allow LDAP passwords and JWT session
          cookies to be intercepted via MITM. Choose ONE of these instead:
          (A) Set EnableHttps=true and provide an ACM certificate ARN — then 0.0.0.0/0
              is fine because traffic is encrypted;
          (B) Restrict AllowedAppCidr to your office/VPN CIDR (e.g., 203.0.113.0/24);
          (C) Restrict to your VPC CIDR and use a VPN to reach it.

  # If HTTPS is enabled, require AcmCertificateArn.
  HttpsRequiresAcmCert:
    RuleCondition: !Equals [!Ref EnableHttps, 'true']
    Assertions:
      - Assert: !Not [!Equals [!Ref AcmCertificateArn, '']]
        AssertDescription: >-
          AcmCertificateArn is required when EnableHttps=true. Create an ACM cert in
          this region for your domain (e.g., refine.yourcompany.com), DNS-validate it,
          then paste the cert ARN here. ACM console: https://console.aws.amazon.com/acm/

  # If HTTPS is enabled, require LoadBalancerSubnetIds to be filled. CF Rules
  # cannot inspect list length directly (Fn::Length not supported in Rules), so
  # we use Fn::Contains to detect the "left at default empty value" case where
  # the CommaDelimitedList resolves to a single empty-string entry. AWS will
  # surface a clear ALB-creation error if fewer than 2 subnets are supplied,
  # which catches the "only one subnet" case that this rule cannot.
  HttpsRequiresLoadBalancerSubnets:
    RuleCondition: !Equals [!Ref EnableHttps, 'true']
    Assertions:
      - Assert: !Not
          - Fn::Contains:
              - !Ref LoadBalancerSubnetIds
              - ''
        AssertDescription: >-
          LoadBalancerSubnetIds is required when EnableHttps=true. Provide at least TWO
          subnet IDs in different Availability Zones (Application Load Balancers require
          multi-AZ). Example: subnet-aaa,subnet-bbb

  # ── BYO consistency rules ──
  # If CreateInstanceProfile=false, the existing-ARN must be provided.
  ByoInstanceProfileNeedsArn:
    RuleCondition: !Equals [!Ref CreateInstanceProfile, 'false']
    Assertions:
      - Assert: !Not [!Equals [!Ref ExistingInstanceProfileArn, '']]
        AssertDescription: 'ExistingInstanceProfileArn is required when CreateInstanceProfile=false. Provide the ARN of an IAM instance profile that includes AmazonSSMManagedInstanceCore + s3:GetObject on the delivery bucket.'

  # If CreateEc2SecurityGroup=false, the existing-ID must be provided.
  ByoEc2SgNeedsId:
    RuleCondition: !Equals [!Ref CreateEc2SecurityGroup, 'false']
    Assertions:
      - Assert: !Not [!Equals [!Ref ExistingEc2SecurityGroupId, '']]
        AssertDescription: 'ExistingEc2SecurityGroupId is required when CreateEc2SecurityGroup=false. The SG must allow inbound TCP 8000 from your ALB SG (HTTPS mode) or AllowedAppCidr (HTTP mode), and outbound to AWS APIs.'

  # If CreateAlbSecurityGroup=false, the existing-ID must be provided AND we
  # must be in BYO ALB mode (no point bringing an SG for an ALB we created).
  ByoAlbSgNeedsId:
    RuleCondition: !Equals [!Ref CreateAlbSecurityGroup, 'false']
    Assertions:
      - Assert: !Not [!Equals [!Ref ExistingAlbSecurityGroupId, '']]
        AssertDescription: 'ExistingAlbSecurityGroupId is required when CreateAlbSecurityGroup=false.'
      - Assert: !Equals [!Ref CreateLoadBalancer, 'false']
        AssertDescription: 'CreateAlbSecurityGroup=false only makes sense when CreateLoadBalancer=false (BYO ALB). If CF is creating the ALB, let CF create its SG too.'

  # If CreateLoadBalancer=false, all four BYO-ALB fields must be filled.
  ByoAlbNeedsAllFields:
    RuleCondition: !And
      - !Equals [!Ref EnableHttps, 'true']
      - !Equals [!Ref CreateLoadBalancer, 'false']
    Assertions:
      - Assert: !Not [!Equals [!Ref ExistingLoadBalancerArn, '']]
        AssertDescription: 'ExistingLoadBalancerArn is required when CreateLoadBalancer=false.'
      - Assert: !Not [!Equals [!Ref ExistingLoadBalancerListenerArn, '']]
        AssertDescription: 'ExistingLoadBalancerListenerArn is required when CreateLoadBalancer=false. Provide the ARN of your existing HTTPS:443 listener; CF will add a listener rule to it.'
      - Assert: !Not [!Equals [!Ref ExistingLoadBalancerHostHeader, '']]
        AssertDescription: 'ExistingLoadBalancerHostHeader is required when CreateLoadBalancer=false. The listener rule routes by host-header match (e.g., refine.yourcompany.com).'

  # Block AllowedSshCidr=0.0.0.0/0 — opening SSH to the world is asking for
  # bot brute-force traffic. If the customer wants to skip SSH, they should
  # leave the field blank (use SSM Session Manager instead).
  BlockPublicSshCidr:
    RuleCondition: !And
      - !Not [!Equals [!Ref AllowedSshCidr, '']]
      - !Equals [!Ref AllowedSshCidr, '0.0.0.0/0']
    Assertions:
      - Assert: !Equals ['false', 'true']
        AssertDescription: >-
          AllowedSshCidr=0.0.0.0/0 is blocked. Either restrict to your office/VPN CIDR,
          or leave AllowedSshCidr BLANK to skip SSH entirely (you can use SSM Session
          Manager from the AWS console — no SSH key or open port required).

Resources:
  # ── S3 Bucket for the delivery ZIP (created by default) ────────────
  DeliveryBucket:
    Type: AWS::S3::Bucket
    Condition: CreateNewBucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Sub 'refine-delivery-${AWS::StackName}-${AWS::AccountId}'
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Application
          Value: Refine
        - Key: ManagedBy
          Value: Blacktip_Refine
        - Key: StackName
          Value: !Ref AWS::StackName
        - Key: Component
          Value: DeliveryStorage
        - Key: Purpose
          Value: 'Storage for Refine delivery ZIP package'
        - Key: Environment
          Value: !Ref EnvironmentTag
        - Key: CreatedBy
          Value: CloudFormation
        - Key: CustomerName
          Value: !Ref CustomerName

  # ── IAM Role: EC2 reads the specific delivery ZIP from S3 ──────────
  RefineEc2Role:
    Type: AWS::IAM::Role
    Condition: CreateNewInstanceProfile
    Properties:
      RoleName: !Sub 'blacktip_refine-ec2-role-${AWS::StackName}'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      # Attach AmazonSSMManagedInstanceCore so customers can connect to the
      # EC2 via Session Manager (browser-based shell, no SSH key required).
      ManagedPolicyArns:
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore'
      Policies:
        - PolicyName: 'DeliveryBucketRead'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                Resource: !If
                  - CreateNewBucket
                  - !Sub 'arn:${AWS::Partition}:s3:::refine-delivery-${AWS::StackName}-${AWS::AccountId}/refine.license'
                  - !Sub 'arn:${AWS::Partition}:s3:::${ExistingBucketName}/${ExistingObjectKey}'
              - Effect: Allow
                Action:
                  - s3:ListBucket
                Resource: !If
                  - CreateNewBucket
                  - !Sub 'arn:${AWS::Partition}:s3:::refine-delivery-${AWS::StackName}-${AWS::AccountId}'
                  - !Sub 'arn:${AWS::Partition}:s3:::${ExistingBucketName}'
        # In-Marketplace fulfillment (MarketplaceFulfillmentMode=Contract): the app
        # verifies the buyer's CONTRACT entitlement via AWS License Manager
        # (CheckoutLicense) - the AWS-prescribed pattern for container products with
        # contract pricing. Harmless when unused (BYOL never calls these). Exact policy
        # per docs.aws.amazon.com/marketplace/.../container-license-manager-integration.
        - PolicyName: 'MarketplaceLicenseManager'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - license-manager:CheckoutLicense
                  - license-manager:GetLicense
                  - license-manager:CheckInLicense
                  - license-manager:ExtendLicenseConsumption
                  - license-manager:ListReceivedLicenses
                Resource: '*'
              # Contract mode, fresh buyer accounts: CheckoutLicense fails with
              # "Service role not found" until the License Manager service-linked
              # role exists. bootstrap.sh creates it idempotently before the
              # container starts (E2E finding, 2026-08-25).
              - Effect: Allow
                Action: iam:CreateServiceLinkedRole
                Resource: 'arn:aws:iam::*:role/aws-service-role/license-manager.amazonaws.com/*'
                Condition:
                  StringLike:
                    iam:AWSServiceName: license-manager.amazonaws.com
      Tags:
        - Key: Application
          Value: Refine
        - Key: ManagedBy
          Value: Blacktip_Refine
        - Key: StackName
          Value: !Ref AWS::StackName
        - Key: Component
          Value: IAM
        - Key: Purpose
          Value: 'EC2 role for downloading delivery package from S3'
        - Key: Environment
          Value: !Ref EnvironmentTag
        - Key: CreatedBy
          Value: CloudFormation
        - Key: CustomerName
          Value: !Ref CustomerName

  RefineEc2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Condition: CreateNewInstanceProfile
    Properties:
      InstanceProfileName: !Sub 'blacktip_refine-ec2-profile-${AWS::StackName}'
      Roles:
        - !Ref RefineEc2Role

  # ── Security Group: SSH + Refine UI ────────────────────────────────
  RefineSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Condition: CreateNewEc2Sg
    Properties:
      GroupName: !Sub 'blacktip_refine-sg-${AWS::StackName}'
      GroupDescription: 'Inbound access for Refine server (SSH if key pair provided + Refine UI)'
      VpcId: !Ref VpcId
      # All ingress is added below as separate conditional resources so each
      # rule can be created or skipped based on EnableHttps / KeyPairName.
      # Port 8000:
      #   - HTTP mode: open to AllowedAppCidr (RefineHttpIngress)
      #   - HTTPS mode: open only to the ALB security group (RefineAlbToEc2Ingress)
      # Port 22:
      #   - Only if KeyPairName + AllowedSshCidr both set (RefineSshIngress)
      # SSM Session Manager uses outbound HTTPS only — no inbound port needed.
      Tags:
        - Key: Application
          Value: Refine
        - Key: ManagedBy
          Value: Blacktip_Refine
        - Key: StackName
          Value: !Ref AWS::StackName
        - Key: Component
          Value: Networking
        - Key: Purpose
          Value: 'Inbound access control for Refine server'
        - Key: Environment
          Value: !Ref EnvironmentTag
        - Key: CreatedBy
          Value: CloudFormation
        - Key: CustomerName
          Value: !Ref CustomerName
        - Key: Name
          Value: !Sub 'blacktip_refine-sg-${AWS::StackName}'

  # SSH ingress — only created if a key pair is provided AND a CIDR is set.
  # If the customer leaves KeyPairName blank, they connect via SSM Session
  # Manager (no inbound port 22 needed).
  # Ingress rules are added only when CF created the EC2 SG. If the customer
  # brings their own SG, they are responsible for ingress configuration there.
  RefineSshIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Condition: AddSshIngress
    Properties:
      GroupId: !Ref RefineSecurityGroup
      IpProtocol: tcp
      FromPort: 22
      ToPort: 22
      CidrIp: !Ref AllowedSshCidr
      Description: 'SSH access'

  # Port 8000 ingress — HTTP mode: open directly to AllowedAppCidr.
  # In HTTPS mode this rule is NOT created (the ALB handles the public side).
  RefineHttpIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Condition: AddHttpIngress
    Properties:
      GroupId: !Ref RefineSecurityGroup
      IpProtocol: tcp
      FromPort: 8000
      ToPort: 8000
      CidrIp: !Ref AllowedAppCidr
      Description: 'Refine UI access (plaintext HTTP - restricted CIDR)'

  # Port 8000 ingress — HTTPS mode: open only to the ALB security group.
  # The EC2 stays private; only the ALB can reach it on the app port.
  # Reference either CF-created or BYO ALB SG depending on CreateAlbSecurityGroup.
  RefineAlbToEc2Ingress:
    Type: AWS::EC2::SecurityGroupIngress
    Condition: AddAlbIngress
    Properties:
      GroupId: !Ref RefineSecurityGroup
      IpProtocol: tcp
      FromPort: 8000
      ToPort: 8000
      SourceSecurityGroupId: !If
        - CreateNewAlbSg
        - !Ref AlbSecurityGroup
        - !Ref ExistingAlbSecurityGroupId
      Description: 'Refine UI access from ALB only (HTTPS terminated at ALB)'

  # ── ALB + HTTPS (conditional on EnableHttps=true and CreateLoadBalancer=true) ─
  # ALB security group: accepts HTTPS from end users (AllowedAppCidr).
  AlbSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Condition: CreateNewAlbSg
    Properties:
      GroupName: !Sub 'blacktip_refine-alb-sg-${AWS::StackName}'
      GroupDescription: 'Inbound HTTPS access for Refine ALB'
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref AllowedAppCidr
          Description: 'HTTPS from end users'
      Tags:
        - Key: Application
          Value: Refine
        - Key: ManagedBy
          Value: Blacktip_Refine
        - Key: StackName
          Value: !Ref AWS::StackName
        - Key: Component
          Value: Networking
        - Key: Purpose
          Value: 'HTTPS inbound for Refine ALB'
        - Key: Environment
          Value: !Ref EnvironmentTag
        - Key: CreatedBy
          Value: CloudFormation
        - Key: CustomerName
          Value: !Ref CustomerName
        - Key: Name
          Value: !Sub 'blacktip_refine-alb-sg-${AWS::StackName}'

  RefineAlb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Condition: CreateNewAlb
    Properties:
      Name: !Sub 'refine-alb-${AWS::StackName}'
      Type: application
      Scheme: !Ref LoadBalancerScheme
      Subnets: !Ref LoadBalancerSubnetIds
      SecurityGroups:
        - !Ref AlbSecurityGroup
      Tags:
        - Key: Application
          Value: Refine
        - Key: ManagedBy
          Value: Blacktip_Refine
        - Key: StackName
          Value: !Ref AWS::StackName
        - Key: Component
          Value: LoadBalancer
        - Key: Purpose
          Value: 'HTTPS termination + reverse proxy to Refine EC2'
        - Key: Environment
          Value: !Ref EnvironmentTag
        - Key: CreatedBy
          Value: CloudFormation
        - Key: CustomerName
          Value: !Ref CustomerName

  RefineTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Condition: HttpsEnabled
    Properties:
      Name: !Sub 'refine-tg-${AWS::StackName}'
      VpcId: !Ref VpcId
      Protocol: HTTP
      Port: 8000
      TargetType: instance
      Targets:
        - Id: !Ref RefineServer
          Port: 8000
      HealthCheckProtocol: HTTP
      HealthCheckPath: /health
      HealthCheckIntervalSeconds: 30
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 3
      Matcher:
        HttpCode: '200'
      Tags:
        - Key: Application
          Value: Refine
        - Key: ManagedBy
          Value: Blacktip_Refine
        - Key: StackName
          Value: !Ref AWS::StackName
        - Key: Component
          Value: LoadBalancer
        - Key: Purpose
          Value: 'ALB target group pointing at Refine EC2'
        - Key: Environment
          Value: !Ref EnvironmentTag
        - Key: CreatedBy
          Value: CloudFormation
        - Key: CustomerName
          Value: !Ref CustomerName

  RefineHttpsListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Condition: CreateNewAlb
    Properties:
      LoadBalancerArn: !Ref RefineAlb
      Protocol: HTTPS
      Port: 443
      Certificates:
        - CertificateArn: !Ref AcmCertificateArn
      SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref RefineTargetGroup

  # BYO ALB: add a host-header listener rule to the customer's existing HTTPS
  # listener. Refine traffic is matched by host (e.g. refine.yourcompany.com)
  # and forwarded to our target group. Other traffic on the listener is
  # untouched.
  RefineExistingListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Condition: UseExistingAlb
    Properties:
      ListenerArn: !Ref ExistingLoadBalancerListenerArn
      Priority: !Ref ExistingLoadBalancerListenerRulePriority
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - !Ref ExistingLoadBalancerHostHeader
      Actions:
        - Type: forward
          TargetGroupArn: !Ref RefineTargetGroup

  # Convenience: redirect HTTP:80 → HTTPS:443 so users typing the bare
  # hostname still land on the secure endpoint.
  RefineHttpRedirectListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Condition: CreateNewAlb
    Properties:
      LoadBalancerArn: !Ref RefineAlb
      Protocol: HTTP
      Port: 80
      DefaultActions:
        - Type: redirect
          RedirectConfig:
            Protocol: HTTPS
            Port: '443'
            StatusCode: HTTP_301

  # ── EC2 Instance with UserData ─────────────────────────────────────
  RefineServer:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      # KeyName is optional — if blank, the EC2 has no SSH access and the
      # customer connects via SSM Session Manager instead.
      KeyName: !If [HasKeyPair, !Ref KeyPairName, !Ref 'AWS::NoValue']
      # When AutoAssignPublicIp=subnet-default (typical), use simple SubnetId
      # + SecurityGroupIds so existing stacks update without instance
      # replacement. When set to 'true' or 'false', use NetworkInterfaces so
      # AssociatePublicIpAddress can be set explicitly (private-subnet
      # deployments need 'false').
      SubnetId: !If [UseExplicitPublicIp, !Ref 'AWS::NoValue', !Ref SubnetId]
      SecurityGroupIds: !If
        - UseExplicitPublicIp
        - !Ref 'AWS::NoValue'
        - - !If [CreateNewEc2Sg, !Ref RefineSecurityGroup, !Ref ExistingEc2SecurityGroupId]
      NetworkInterfaces: !If
        - UseExplicitPublicIp
        - - DeviceIndex: '0'
            SubnetId: !Ref SubnetId
            GroupSet:
              - !If [CreateNewEc2Sg, !Ref RefineSecurityGroup, !Ref ExistingEc2SecurityGroupId]
            AssociatePublicIpAddress: !If [ForcePublicIp, true, false]
        - !Ref 'AWS::NoValue'
      IamInstanceProfile: !If
        - CreateNewInstanceProfile
        - !Ref RefineEc2InstanceProfile
        - !Ref ExistingInstanceProfileArn
      ImageId: !If
        - UseUbuntu
        - '{{resolve:ssm:/aws/service/canonical/ubuntu/server/22.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}'
        - !If
          - UseAmazonLinux
          - '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
          - !Ref CustomAmiId
      BlockDeviceMappings:
        # Device name varies by OS: Ubuntu uses /dev/sda1, Amazon Linux 2023 uses /dev/xvda
        - DeviceName: !If [UseAmazonLinux, '/dev/xvda', '/dev/sda1']
          Ebs:
            VolumeSize: !Ref RootVolumeSize
            VolumeType: gp3
            Encrypted: true
            DeleteOnTermination: true
      UserData:
        Fn::Base64:
          Fn::Sub:
            - |
              #!/bin/bash
              set -euxo pipefail
              exec > >(tee /var/log/refine-userdata.log) 2>&1
              # Full bootstrap is hosted in S3 to keep UserData under EC2 s 16 KB limit.
              # Pass CFN parameters to it as env vars, then download + run it.
              export REFINE_BUCKET="${ResolvedBucketName}"
              export REFINE_KEY="${ResolvedObjectKey}"
              export REFINE_IMAGE_URI="${RefineImageUri}"
              export MARKETPLACE_FULFILLMENT_MODE="${MarketplaceFulfillmentMode}"
              export REFINE_MP_PRODUCT_SKU="${RefineMpProductSku}"
              export REFINE_MP_ENTITLEMENTS="${RefineMpEntitlements}"
              export ENABLE_AUTO_UPDATE="${EnableAutoUpdate}"
              export AUTO_UPDATE_SCHEDULE="${AutoUpdateSchedule}"
              export AUTO_UPDATE_NOTIFY_EMAIL="${AutoUpdateNotifyEmail}"
              export ENABLE_ROLLBACK="${EnableRollbackOnHealthFailure}"
              export REFINE_ADMIN_EMAIL="${AdminEmail}"
              export REFINE_ADMIN_PASSWORD="${AdminPassword}"
              export START_IN_DEMO_MODE="${StartInDemoMode}"
              for i in 1 2 3 4 5; do
                curl -fsSL "${BootstrapUrl}" -o /opt/refine-bootstrap.sh && break
                echo "bootstrap download attempt $i failed; retry in 10s"; sleep 10
              done
              bash /opt/refine-bootstrap.sh
            - ResolvedBucketName: !If
                - CreateNewBucket
                - !Ref DeliveryBucket
                - !Ref ExistingBucketName
              ResolvedObjectKey: !If
                - CreateNewBucket
                - 'refine.license'
                - !Ref ExistingObjectKey
      Tags:
        - Key: Application
          Value: Refine
        - Key: ManagedBy
          Value: Blacktip_Refine
        - Key: StackName
          Value: !Ref AWS::StackName
        - Key: Component
          Value: RefineServer
        - Key: Purpose
          Value: 'Refine application server (Docker host)'
        - Key: Environment
          Value: !Ref EnvironmentTag
        - Key: CreatedBy
          Value: CloudFormation
        - Key: CustomerName
          Value: !Ref CustomerName
        - Key: Name
          Value: !Sub 'refine-server-${AWS::StackName}'

  # ── Elastic IP (conditional) ───────────────────────────────────────
  RefineEip:
    Type: AWS::EC2::EIP
    Condition: HasEip
    Properties:
      Domain: vpc
      Tags:
        - Key: Application
          Value: Refine
        - Key: ManagedBy
          Value: Blacktip_Refine
        - Key: StackName
          Value: !Ref AWS::StackName
        - Key: Component
          Value: RefineServer
        - Key: Purpose
          Value: 'Stable public IP for Refine server'
        - Key: Environment
          Value: !Ref EnvironmentTag
        - Key: CreatedBy
          Value: CloudFormation
        - Key: CustomerName
          Value: !Ref CustomerName
        - Key: Name
          Value: !Sub 'refine-eip-${AWS::StackName}'

  RefineEipAssociation:
    Type: AWS::EC2::EIPAssociation
    Condition: HasEip
    Properties:
      AllocationId: !GetAtt RefineEip.AllocationId
      InstanceId: !Ref RefineServer

Outputs:
  NextStep:
    Condition: IsByolFulfillment
    Description: '👋 What to do next — read this first.'
    Value: !Sub
      - 'Refine is provisioned and the Docker image is already pulled from public ECR — only your license is needed. Go to https://blacktip-ops.com/marketplace-license and submit the BYOL request with: (1) your AWS Marketplace customer ID, (2) DeliveryBucketName = ${Bucket}, (3) contact email. Blacktip Solutions uploads your Ed25519 license file (refine.license) to that bucket; an automatic watcher on the EC2 detects it and restarts the container. Typical end-to-end: < 1 business hour from this stack reaching CREATE_COMPLETE to the Refine UI loading at RefineUrl.'
      - Bucket: !If [CreateNewBucket, !Ref DeliveryBucket, !Ref ExistingBucketName]

  NextStepContract:
    Condition: IsContractFulfillment
    Description: '👋 What to do next — read this first.'
    Value: 'Refine is provisioned and licensed automatically through your AWS Marketplace contract — no license file, form, or manual step is needed. Open RefineUrl (see Outputs) and log in with the AdminEmail/AdminPassword you set when launching this stack.'

  LicenseRequestUrl:
    Condition: IsByolFulfillment
    Description: 'BYOL license-request form (submit this to receive your license).'
    Value: 'https://blacktip-ops.com/marketplace-license'

  MarketplaceFulfillmentMode:
    Description: 'Current fulfillment mode. Contract = licensed automatically via your AWS Marketplace entitlement (License Manager). BYOL = Blacktip issues an Ed25519 license via the S3 bucket.'
    Value: !Ref MarketplaceFulfillmentMode

  ServerInstanceId:
    Description: 'EC2 instance ID'
    Value: !Ref RefineServer

  ServerPublicIp:
    Description: 'Public IP of the Refine server (EIP if enabled, else instance public IP)'
    Value: !If [HasEip, !Ref RefineEip, !GetAtt RefineServer.PublicIp]

  RefineUrl:
    Description: 'URL for end users to access Refine.'
    Value: !If
      - CreateNewAlb
      - !Sub
        - 'https://${AlbDns}    (configure DNS to point your domain at this ALB)'
        - AlbDns: !GetAtt RefineAlb.DNSName
      - !If
        - UseExistingAlb
        - !Sub 'https://${ExistingLoadBalancerHostHeader}    (your existing ALB at the host header you specified)'
        - !Sub
          - 'http://${PublicIp}:8000'
          - PublicIp: !If [HasEip, !Ref RefineEip, !GetAtt RefineServer.PublicIp]

  AlbDnsName:
    Condition: CreateNewAlb
    Description: 'DNS name of the new Application Load Balancer. Create a CNAME record in your DNS provider pointing your domain (e.g., refine.yourcompany.com) at this name. For internal-scheme ALBs the DNS resolves only inside your VPC.'
    Value: !GetAtt RefineAlb.DNSName

  AlbHostedZoneId:
    Condition: CreateNewAlb
    Description: 'Hosted Zone ID of the new ALB. Use this if creating a Route 53 alias record (instead of a CNAME).'
    Value: !GetAtt RefineAlb.CanonicalHostedZoneID

  DnsSetupInstructions:
    Condition: CreateNewAlb
    Description: 'How to point your domain at the ALB'
    Value: !Sub
      - 'In your DNS provider, create a CNAME record: refine.yourcompany.com -> ${AlbDns}. Or in Route 53, create an alias A record pointing to the ALB.'
      - AlbDns: !GetAtt RefineAlb.DNSName

  ExistingAlbListenerRule:
    Condition: UseExistingAlb
    Description: 'BYO ALB: a listener rule was added to your existing HTTPS listener. Traffic with host header matching ExistingLoadBalancerHostHeader is forwarded to the new Refine target group.'
    Value: !Sub 'Listener rule added to ${ExistingLoadBalancerListenerArn} with host-header ${ExistingLoadBalancerHostHeader} (priority ${ExistingLoadBalancerListenerRulePriority}). Make sure your DNS already points at this ALB.'

  AdminConnectMethod:
    Description: 'How the admin connects to the EC2 to run setup (this is for the admin only — end users use RefineUrl instead).'
    Value: !If
      - HasKeyPair
      - !Sub
        - 'SSH: ssh -i <your-key>.pem ${SshUser}@${PublicIp}    OR    SSM Session Manager (browser): https://console.aws.amazon.com/systems-manager/session-manager/${InstanceId}'
        - SshUser: !If [UseAmazonLinux, 'ec2-user', 'ubuntu']
          PublicIp: !If [HasEip, !Ref RefineEip, !GetAtt RefineServer.PublicIp]
          InstanceId: !Ref RefineServer
      - !Sub
        - 'SSM Session Manager (browser, no SSH key needed): https://console.aws.amazon.com/systems-manager/session-manager/${InstanceId}'
        - InstanceId: !Ref RefineServer

  DeliveryBucketName:
    Condition: IsByolFulfillment
    Description: 'S3 bucket where Blacktip Solutions uploads your refine.license file (paste this into the BYOL request form).'
    Value: !If
      - CreateNewBucket
      - !Ref DeliveryBucket
      - !Ref ExistingBucketName

  DeliveryUploadKey:
    Condition: IsByolFulfillment
    Description: 'Object key the license-watcher polls for.'
    Value: !If [CreateNewBucket, 'refine.license', !Ref ExistingObjectKey]

  UploadCommand:
    Condition: IsByolFulfillment
    Description: 'CLI command Blacktip Solutions runs to deliver your license file. Customer does not run this.'
    Value: !Sub
      - 'aws s3 cp <refine.license> s3://${Bucket}/${Key}'
      - Bucket: !If [CreateNewBucket, !Ref DeliveryBucket, !Ref ExistingBucketName]
        Key: !If [CreateNewBucket, 'refine.license', !Ref ExistingObjectKey]

  AdminLogin:
    Description: 'How to log in to Refine (no host setup needed - the container starts itself).'
    Value: !If
      - IsByolFulfillment
      - !Sub 'No manual steps on the host are required: the container started automatically (in DEMO mode) and the on-host license-watcher installs your license from the bucket the moment it arrives (see the NextStep output). To LOG IN at RefineUrl -> email: "${AdminEmail}" ; password: the AdminPassword you set when launching this stack. If you left AdminPassword blank, a random password was generated - retrieve it on the host (see AdminConnectMethod) with:  sudo docker logs refine 2>&1 | grep -A4 "FIRST-RUN ADMIN"  (change it after logging in).'
      - !Sub 'No manual steps on the host are required: the container started automatically and is licensed through your AWS Marketplace contract. To LOG IN at RefineUrl -> email: "${AdminEmail}" ; password: the AdminPassword you set when launching this stack. If you left AdminPassword blank, a random password was generated - retrieve it on the host (see AdminConnectMethod) with:  sudo docker logs refine 2>&1 | grep -A4 "FIRST-RUN ADMIN"  (change it after logging in).'
