/**
 * Copyright IBM Corp. 2016, 2025
 * SPDX-License-Identifier: BUSL-1.1
 */

import Model, { attr } from '@ember-data/model';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { withFormFields } from 'vault/decorators/model-form-fields';
import { withModelValidations } from 'vault/decorators/model-validations';

const validations = {
  type: [{ type: 'presence', message: 'Type is required.' }],
  commonName: [{ type: 'presence', message: 'Common name is required.' }],
  issuerName: [
    {
      validator(model) {
        if (
          (model.actionType === 'generate-root' || model.actionType === 'rotate-root') &&
          model.issuerName === 'default'
        )
          return false;
        return true;
      },
      message: `Issuer name must be unique across all issuers and not be the reserved value 'default'.`,
    },
  ],
  keyName: [
    {
      validator(model) {
        if (model.keyName === 'default') return false;
        return true;
      },
      message: `Key name cannot be the reserved value 'default'`,
    },
  ],
};

/**
 * This model maps to multiple PKI endpoints, specifically the ones that make up the
 * configuration/create workflow. These endpoints also share a nontypical behavior in that
 * a POST request to the endpoints don't necessarily result in a single entity created --
 * depending on the inputs, some number of issuers, keys, and certificates can be created
 * from the API.
 */
@withModelValidations(validations)
@withFormFields()
export default class PkiActionModel extends Model {
  @service secretMountPath;

  @tracked actionType; // used to toggle between different form fields when creating configuration

  /* actionType import */
  @attr('string') pemBundle;

  // parsed attrs from parse-pki-cert util if certificate on response
  @attr parsedCertificate;

  // readonly attrs returned after importing
  @attr importedIssuers;
  @attr importedKeys;
  @attr mapping;
  @attr('string', { readOnly: true, isCertificate: true }) certificate;

  /* actionType generate-root */

  // readonly attrs returned right after root generation
  @attr serialNumber;
  @attr('string', { label: 'Issuing CA', readOnly: true, isCertificate: true }) issuingCa;
  // end of readonly

  @attr('string', {
    possibleValues: ['exported', 'internal', 'existing', 'kms'],
    noDefault: true,
  })
  type;

  @attr('string') issuerName;

  @attr('string') keyName;

  @attr('string', {
    defaultValue: 'default',
    label: 'Key reference',
  })
  keyRef; // type=existing only

  @attr('string') commonName; // REQUIRED

  @attr('string', {
    label: 'Subject Alternative Names (SANs)',
    editType: 'stringArray',
  })
  altNames;

  @attr('string', {
    label: 'IP Subject Alternative Names (IP SANs)',
    editType: 'stringArray',
  })
  ipSans;

  @attr('string', {
    label: 'URI Subject Alternative Names (URI SANs)',
    editType: 'stringArray',
  })
  uriSans;

  @attr('string', {
    label: 'Other SANs',
    editType: 'stringArray',
  })
  otherSans;

  @attr('string', {
    defaultValue: 'pem',
    possibleValues: ['pem', 'der', 'pem_bundle'],
  })
  format;

  @attr('string', {
    defaultValue: 'der',
    possibleValues: ['der', 'pkcs8'],
  })
  privateKeyFormat;

  @attr('string', {
    defaultValue: 'rsa',
    possibleValues: ['rsa', 'ed25519', 'ec'],
  })
  keyType;

  @attr('string', {
    defaultValue: '0',
    // options management happens in pki-key-parameters
  })
  keyBits;

  @attr('number', {
    defaultValue: -1,
  })
  maxPathLength;

  @attr('boolean', {
    label: 'Exclude common name from SANs',
    subText:
      'If checked, the common name will not be included in DNS or Email Subject Alternate Names. This is useful if the CN is a human-readable identifier, not a hostname or email address.',
    defaultValue: false,
  })
  excludeCnFromSans;

  @attr('string', {
    label: 'Permitted DNS domains',
  })
  permittedDnsDomains;

  @attr('string', {
    label: 'Organizational Units (OU)',
    subText:
      'A list of allowed serial numbers to be requested during certificate issuance. Shell-style globbing is supported. If empty, custom-specified serial numbers will be forbidden.',
    editType: 'stringArray',
  })
  ou;
  @attr({ editType: 'stringArray' }) organization;
  @attr({ editType: 'stringArray' }) country;
  @attr({ editType: 'stringArray' }) locality;
  @attr({ editType: 'stringArray' }) province;
  @attr({ editType: 'stringArray' }) streetAddress;
  @attr({ editType: 'stringArray' }) postalCode;

  @attr('string', {
    subText:
      "Specifies the requested Subject's named Serial Number value. This has no impact on the Certificate's serial number randomly generated by Vault.",
  })
  subjectSerialNumber;
  // this is different from the number (16:5e:a0...) randomly generated by Vault
  // https://developer.hashicorp.com/vault/api-docs/secret/pki#serial_number

  @attr('boolean', {
    subText: 'Whether to add a Basic Constraints extension with CA: true.',
  })
  addBasicConstraints;

  @attr({
    label: 'Backdate validity',
    detailsLabel: 'Issued certificate backdating',
    helperTextDisabled: 'Vault will use the default value, 30s',
    helperTextEnabled:
      'Also called the not_before_duration property. Allows certificates to be valid for a certain time period before now. This is useful to correct clock misalignment on various systems when setting up your CA.',
    editType: 'ttl',
    defaultValue: '30s',
  })
  notBeforeDuration;

  @attr('string') managedKeyName;
  @attr('string', {
    label: 'Managed key UUID',
  })
  managedKeyId;

  @attr({
    label: 'Not valid after',
    detailsLabel: 'Issued certificates expire after',
    subText:
      'The time after which this certificate will no longer be valid. This can be a TTL (a range of time from now) or a specific date.',
    editType: 'yield',
  })
  customTtl;
  @attr('string') ttl;
  @attr('date') notAfter;

  @attr('string', { label: 'Issuer ID', readOnly: true, detailLinkTo: 'issuers.issuer.details' }) issuerId; // returned from generate-root action

  // For generating and signing a CSR
  @attr('string', { label: 'CSR', isCertificate: true }) csr;
  @attr caChain;
  @attr('string', { label: 'Key ID', detailLinkTo: 'keys.key.details' }) keyId;
  @attr('string', { isCertificate: true }) privateKey;
  @attr('string') privateKeyType;

  get backend() {
    return this.secretMountPath.currentPath;
  }

  // To determine which endpoint the config adapter should use,
  // we want to check capabilities on the newer endpoints (those
  // prefixed with "issuers") and use the old path as fallback
  // if user does not have permissions.
  @lazyCapabilities(apiPath`${'backend'}/issuers/import/bundle`, 'backend') importBundlePath;
  @lazyCapabilities(apiPath`${'backend'}/issuers/generate/root/${'type'}`, 'backend', 'type')
  generateIssuerRootPath;
  @lazyCapabilities(apiPath`${'backend'}/issuers/generate/intermediate/${'type'}`, 'backend', 'type')
  generateIssuerCsrPath;
  @lazyCapabilities(apiPath`${'backend'}/issuers/cross-sign`, 'backend') crossSignPath;

  get canImportBundle() {
    return this.importBundlePath.get('canCreate') === true;
  }
  get canGenerateIssuerRoot() {
    return this.generateIssuerRootPath.get('canCreate') === true;
  }
  get canGenerateIssuerIntermediate() {
    return this.generateIssuerCsrPath.get('canCreate') === true;
  }
  get canCrossSign() {
    return this.crossSignPath.get('canCreate') === true;
  }
}
