import { Component, OnDestroy, OnInit } from "@angular/core";
import {
  AbstractControl,
  FormBuilder,
  FormControl,
  UntypedFormGroup,
  Validators,
} from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { concatMap, Subject, takeUntil } from "rxjs";

import { ControlsOf } from "@bitwarden/angular/types/controls-of";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import {
  MemberDecryptionType,
  OpenIdConnectRedirectBehavior,
  Saml2BindingType,
  Saml2NameIdFormat,
  Saml2SigningBehavior,
  SsoType,
} from "@bitwarden/common/auth/enums/sso";
import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api";
import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request";
import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response";
import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";

import { ssoTypeValidator } from "./sso-type.validator";

interface SelectOptions {
  name: string;
  value: any;
  disabled?: boolean;
}

const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";

@Component({
  selector: "app-org-manage-sso",
  templateUrl: "sso.component.html",
})
export class SsoComponent implements OnInit, OnDestroy {
  readonly ssoType = SsoType;
  readonly memberDecryptionType = MemberDecryptionType;

  readonly ssoTypeOptions: SelectOptions[] = [
    { name: this.i18nService.t("selectType"), value: SsoType.None, disabled: true },
    { name: "OpenID Connect", value: SsoType.OpenIdConnect },
    { name: "SAML 2.0", value: SsoType.Saml2 },
  ];

  readonly samlSigningAlgorithms = [
    "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
    "http://www.w3.org/2000/09/xmldsig#rsa-sha384",
    "http://www.w3.org/2000/09/xmldsig#rsa-sha512",
  ];

  readonly saml2SigningBehaviourOptions: SelectOptions[] = [
    {
      name: "If IdP Wants Authn Requests Signed",
      value: Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned,
    },
    { name: "Always", value: Saml2SigningBehavior.Always },
    { name: "Never", value: Saml2SigningBehavior.Never },
  ];
  readonly saml2BindingTypeOptions: SelectOptions[] = [
    { name: "Redirect", value: Saml2BindingType.HttpRedirect },
    { name: "HTTP POST", value: Saml2BindingType.HttpPost },
  ];
  readonly saml2NameIdFormatOptions: SelectOptions[] = [
    { name: "Not Configured", value: Saml2NameIdFormat.NotConfigured },
    { name: "Unspecified", value: Saml2NameIdFormat.Unspecified },
    { name: "Email Address", value: Saml2NameIdFormat.EmailAddress },
    { name: "X.509 Subject Name", value: Saml2NameIdFormat.X509SubjectName },
    { name: "Windows Domain Qualified Name", value: Saml2NameIdFormat.WindowsDomainQualifiedName },
    { name: "Kerberos Principal Name", value: Saml2NameIdFormat.KerberosPrincipalName },
    { name: "Entity Identifier", value: Saml2NameIdFormat.EntityIdentifier },
    { name: "Persistent", value: Saml2NameIdFormat.Persistent },
    { name: "Transient", value: Saml2NameIdFormat.Transient },
  ];

  readonly connectRedirectOptions: SelectOptions[] = [
    { name: "Redirect GET", value: OpenIdConnectRedirectBehavior.RedirectGet },
    { name: "Form POST", value: OpenIdConnectRedirectBehavior.FormPost },
  ];

  private destroy$ = new Subject<void>();
  showTdeOptions = false;
  showKeyConnectorOptions = false;

  showOpenIdCustomizations = false;

  loading = true;
  haveTestedKeyConnector = false;
  organizationId: string;
  organization: Organization;

  callbackPath: string;
  signedOutCallbackPath: string;
  spEntityId: string;
  spEntityIdStatic: string;
  spMetadataUrl: string;
  spAcsUrl: string;

  protected openIdForm = this.formBuilder.group<ControlsOf<SsoConfigView["openId"]>>(
    {
      authority: new FormControl("", Validators.required),
      clientId: new FormControl("", Validators.required),
      clientSecret: new FormControl("", Validators.required),
      metadataAddress: new FormControl(),
      redirectBehavior: new FormControl(
        OpenIdConnectRedirectBehavior.RedirectGet,
        Validators.required,
      ),
      getClaimsFromUserInfoEndpoint: new FormControl(),
      additionalScopes: new FormControl(),
      additionalUserIdClaimTypes: new FormControl(),
      additionalEmailClaimTypes: new FormControl(),
      additionalNameClaimTypes: new FormControl(),
      acrValues: new FormControl(),
      expectedReturnAcrValue: new FormControl(),
    },
    {
      updateOn: "blur",
    },
  );

  protected samlForm = this.formBuilder.group<ControlsOf<SsoConfigView["saml"]>>(
    {
      spUniqueEntityId: new FormControl(true, { updateOn: "change" }),
      spNameIdFormat: new FormControl(Saml2NameIdFormat.NotConfigured),
      spOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm),
      spSigningBehavior: new FormControl(Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned),
      spMinIncomingSigningAlgorithm: new FormControl(defaultSigningAlgorithm),
      spWantAssertionsSigned: new FormControl(),
      spValidateCertificates: new FormControl(),

      idpEntityId: new FormControl("", Validators.required),
      idpBindingType: new FormControl(Saml2BindingType.HttpRedirect),
      idpSingleSignOnServiceUrl: new FormControl(),
      idpSingleLogoutServiceUrl: new FormControl(),
      idpX509PublicCert: new FormControl("", Validators.required),
      idpOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm),
      idpAllowUnsolicitedAuthnResponse: new FormControl(),
      idpAllowOutboundLogoutRequests: new FormControl(true),
      idpWantAuthnRequestsSigned: new FormControl(),
    },
    {
      updateOn: "blur",
    },
  );

  protected ssoConfigForm = this.formBuilder.group<ControlsOf<SsoConfigView>>({
    configType: new FormControl(SsoType.None),
    memberDecryptionType: new FormControl(MemberDecryptionType.MasterPassword),
    keyConnectorUrl: new FormControl(""),
    openId: this.openIdForm,
    saml: this.samlForm,
    enabled: new FormControl(false),
    ssoIdentifier: new FormControl("", {
      validators: [Validators.maxLength(50), Validators.required],
    }),
  });

  get enabledCtrl() {
    return this.ssoConfigForm?.controls?.enabled as FormControl;
  }
  get ssoIdentifierCtrl() {
    return this.ssoConfigForm?.controls?.ssoIdentifier as FormControl;
  }
  get configTypeCtrl() {
    return this.ssoConfigForm?.controls?.configType as FormControl;
  }

  constructor(
    private formBuilder: FormBuilder,
    private route: ActivatedRoute,
    private apiService: ApiService,
    private platformUtilsService: PlatformUtilsService,
    private i18nService: I18nService,
    private organizationService: OrganizationService,
    private organizationApiService: OrganizationApiServiceAbstraction,
    private configService: ConfigService,
  ) {}

  async ngOnInit() {
    this.enabledCtrl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((enabled) => {
      if (enabled) {
        this.ssoIdentifierCtrl.setValidators([Validators.maxLength(50), Validators.required]);
        this.configTypeCtrl.setValidators([
          ssoTypeValidator(this.i18nService.t("selectionIsRequired")),
        ]);
      } else {
        this.ssoIdentifierCtrl.setValidators([]);
        this.configTypeCtrl.setValidators([]);
      }

      this.ssoIdentifierCtrl.updateValueAndValidity();
      this.configTypeCtrl.updateValueAndValidity();
    });

    this.ssoConfigForm
      .get("configType")
      .valueChanges.pipe(takeUntil(this.destroy$))
      .subscribe((newType: SsoType) => {
        if (newType === SsoType.OpenIdConnect) {
          this.openIdForm.enable();
          this.samlForm.disable();
        } else if (newType === SsoType.Saml2) {
          this.openIdForm.disable();
          this.samlForm.enable();
        } else {
          this.openIdForm.disable();
          this.samlForm.disable();
        }
      });

    this.samlForm
      .get("spSigningBehavior")
      .valueChanges.pipe(takeUntil(this.destroy$))
      .subscribe(() => this.samlForm.get("idpX509PublicCert").updateValueAndValidity());

    this.route.params
      .pipe(
        concatMap(async (params) => {
          this.organizationId = params.organizationId;
          await this.load();
        }),
        takeUntil(this.destroy$),
      )
      .subscribe();

    this.showKeyConnectorOptions = this.platformUtilsService.isSelfHost();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  async load() {
    this.organization = await this.organizationService.get(this.organizationId);
    const ssoSettings = await this.organizationApiService.getSso(this.organizationId);
    this.populateForm(ssoSettings);

    this.callbackPath = ssoSettings.urls.callbackPath;
    this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
    this.spEntityId = ssoSettings.urls.spEntityId;
    this.spEntityIdStatic = ssoSettings.urls.spEntityIdStatic;
    this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
    this.spAcsUrl = ssoSettings.urls.spAcsUrl;

    this.loading = false;
  }

  submit = async () => {
    this.updateFormValidationState(this.ssoConfigForm);

    if (this.ssoConfigForm.value.memberDecryptionType === MemberDecryptionType.KeyConnector) {
      this.haveTestedKeyConnector = false;
      await this.validateKeyConnectorUrl();
    }

    if (!this.ssoConfigForm.valid) {
      this.readOutErrors();
      return;
    }
    const request = new OrganizationSsoRequest();
    request.enabled = this.enabledCtrl.value;
    // Return null instead of empty string to avoid duplicate id errors in database
    request.identifier = this.ssoIdentifierCtrl.value === "" ? null : this.ssoIdentifierCtrl.value;
    request.data = SsoConfigApi.fromView(this.ssoConfigForm.getRawValue());

    const response = await this.organizationApiService.updateSso(this.organizationId, request);
    this.populateForm(response);

    this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
  };

  async validateKeyConnectorUrl() {
    if (this.haveTestedKeyConnector) {
      return;
    }

    this.keyConnectorUrl.markAsPending();

    try {
      await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
      this.keyConnectorUrl.updateValueAndValidity();
    } catch {
      this.keyConnectorUrl.setErrors({
        invalidUrl: { message: this.i18nService.t("keyConnectorTestFail") },
      });
    }

    this.haveTestedKeyConnector = true;
  }

  toggleOpenIdCustomizations() {
    this.showOpenIdCustomizations = !this.showOpenIdCustomizations;
  }

  getErrorCount(form: UntypedFormGroup): number {
    return Object.values(form.controls).reduce((acc: number, control: AbstractControl) => {
      if (control instanceof UntypedFormGroup) {
        return acc + this.getErrorCount(control);
      }

      if (control.errors == null) {
        return acc;
      }
      return acc + Object.keys(control.errors).length;
    }, 0);
  }

  get enableTestKeyConnector() {
    return (
      this.ssoConfigForm.value?.memberDecryptionType === MemberDecryptionType.KeyConnector &&
      !Utils.isNullOrWhitespace(this.keyConnectorUrl?.value)
    );
  }

  get keyConnectorUrl() {
    return this.ssoConfigForm.get("keyConnectorUrl");
  }

  get samlSigningAlgorithmOptions(): SelectOptions[] {
    return this.samlSigningAlgorithms.map((algorithm) => ({ name: algorithm, value: algorithm }));
  }

  /**
   * Shows any validation errors for the form by marking all controls as dirty and touched.
   * If nested form groups are found, they are also updated.
   * @param form - the form to show validation errors for
   */
  private updateFormValidationState(form: UntypedFormGroup) {
    Object.values(form.controls).forEach((control: AbstractControl) => {
      if (control.disabled) {
        return;
      }

      if (control instanceof UntypedFormGroup) {
        this.updateFormValidationState(control);
      } else {
        control.markAsDirty();
        control.markAsTouched();
        control.updateValueAndValidity();
      }
    });
  }

  private populateForm(orgSsoResponse: OrganizationSsoResponse) {
    const ssoConfigView = new SsoConfigView(orgSsoResponse);
    this.ssoConfigForm.patchValue(ssoConfigView);
  }

  private readOutErrors() {
    const errorText = this.i18nService.t("error");
    const errorCount = this.getErrorCount(this.ssoConfigForm);
    const errorCountText = this.i18nService.t(
      errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural",
      errorCount.toString(),
    );

    const div = document.createElement("div");
    div.className = "sr-only";
    div.id = "srErrorCount";
    div.setAttribute("aria-live", "polite");
    div.innerText = errorText + ": " + errorCountText;

    const existing = document.getElementById("srErrorCount");
    if (existing != null) {
      existing.remove();
    }

    document.body.append(div);
  }
}
