/*
 * Copyright 2025 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Error as ErrorIcon } from "@blueprintjs/icons";
import classNames from "classnames";
import React, { memo, useCallback, useMemo, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { ActionButton } from "../base-components/action-button/ActionButton.js";
import { SkeletonBar } from "../base-components/skeleton/SkeletonBar.js";
import { Tooltip } from "../base-components/tooltip/Tooltip.js";
import { useAsyncAction } from "../shared/hooks/useAsyncAction.js";
import { FieldBridge } from "./fields/FieldBridge.js";
import { FormHeader } from "./FormHeader.js";
import { FormSection } from "./FormSection.js";
import styles from "./BaseForm.module.css";
export const BaseForm = /*#__PURE__*/memo(function ({
  formTitle,
  formContent,
  formState: controlledFormState,
  onFieldValueChange,
  onSubmit,
  isSubmitDisabled = false,
  isPending = false,
  isLoading = false,
  className,
  submitButtonText = "Submit",
  submitButtonVariant = "primary"
}) {
  const portalContainerRef = useRef(null);
  const allFieldDefinitions = useMemo(() => flattenFieldDefinitions(formContent), [formContent]);
  const defaultValues = useMemo(() => buildDefaultValues(allFieldDefinitions), [allFieldDefinitions]);
  const {
    control,
    trigger,
    getValues,
    formState: {
      errors
    }
  } = useForm({
    // Validate on blur first, then revalidate on change after the first
    // error. This gives the user a chance to finish typing before seeing
    // errors, while staying responsive once an error is surfaced.
    mode: "onTouched",
    ...(controlledFormState != null ? {
      values: controlledFormState
    } : {
      defaultValues
    })
  });
  const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);
  const {
    isPending: isSubmitting,
    error: submissionError,
    execute: executeSubmit,
    clearError
  } = useAsyncAction(onSubmit);
  const submissionErrorMessage = submissionError != null ? submissionError instanceof Error ? submissionError.message :
  // TODO: provide better error message
  "Submission failed" : undefined;
  const submitForm = useCallback(async () => {
    setHasAttemptedSubmit(true);
    const isValid = await trigger();
    if (!isValid) {
      return;
    }

    // In controlled mode, always submit the controlled state, not RHF's
    // internal state. Between a user keystroke and the parent re-rendering,
    // RHF's store may hold the user-typed value rather than the parent's
    // value. Using controlledFormState directly preserves the existing
    // guarantee that controlled mode submits the parent's state.
    await executeSubmit(controlledFormState ?? getValues());
  }, [trigger, executeSubmit, controlledFormState, getValues]);
  const handleFieldChange = useCallback((fieldKey, value) => {
    clearError();
    onFieldValueChange?.(fieldKey, value);
  }, [clearError, onFieldValueChange]);
  const labelByFieldKey = useMemo(() => new Map(allFieldDefinitions.map(d => [d.fieldKey, d.label])), [allFieldDefinitions]);

  // RHF reuses the same errors object reference across renders so we cannot memoize errorEntries
  const errorEntries = Object.entries(errors).map(([key, entry]) => ({
    label: labelByFieldKey.get(key) ?? key,
    message: entry?.message ?? "Invalid"
  }));
  const areErrorsPresent = errorEntries.length > 0;
  const buttonErrorMessage = areErrorsPresent ? "Some fields are invalid" : submissionErrorMessage;
  return /*#__PURE__*/React.createElement("form", {
    ref: portalContainerRef,
    className: classNames(styles.osdkForm, className)
    // Workshop widgets can run in iframes without `allow-forms`, where
    // native form submission is blocked. Keep the form landmark, but do not
    // wire any form-level submit handlers; the submit button invokes our
    // JavaScript submit path directly.
  }, formTitle != null && /*#__PURE__*/React.createElement(FormHeader, {
    title: formTitle
  }), isLoading && allFieldDefinitions.length === 0 && /*#__PURE__*/React.createElement("div", {
    role: "status",
    "aria-label": "Loading form fields",
    className: styles.osdkFormFields
  }, FORM_SKELETON), /*#__PURE__*/React.createElement("div", {
    className: styles.osdkFormFields
  }, formContent.map(item => {
    if (item.type === "field") {
      return /*#__PURE__*/React.createElement(FieldBridge, {
        key: item.definition.fieldKey,
        fieldDef: item.definition,
        control: control,
        onExternalChange: handleFieldChange,
        portalContainer: portalContainerRef
      });
    }
    const sectionErrorCount = item.definition.fields.reduce((count, field) => count + (errors[field.fieldKey] != null ? 1 : 0), 0);
    return /*#__PURE__*/React.createElement(FormSection, {
      key: item.key,
      definition: item.definition,
      errorCount: sectionErrorCount
    }, item.definition.fields.map(fieldDef => /*#__PURE__*/React.createElement(FieldBridge, {
      key: fieldDef.fieldKey,
      fieldDef: fieldDef,
      control: control,
      onExternalChange: handleFieldChange,
      portalContainer: portalContainerRef
    })));
  })), /*#__PURE__*/React.createElement("div", {
    className: styles.osdkFormFooter
  }, /*#__PURE__*/React.createElement(ErrorIndicator, {
    errorEntries: errorEntries
  }), /*#__PURE__*/React.createElement("div", {
    className: styles.osdkFormSubmitButton
  }, /*#__PURE__*/React.createElement(SubmitButton, {
    isPending: isPending || isSubmitting,
    isSubmitDisabled: isSubmitDisabled || hasAttemptedSubmit && areErrorsPresent,
    errorMessage: buttonErrorMessage,
    buttonText: submitButtonText,
    buttonVariant: submitButtonVariant,
    onClick: submitForm
  }))));
});

/**
 * Extracts all RendererFieldDefinitions from formContent, flattening
 * section fields into a single array. RHF sees a flat field namespace
 * regardless of visual grouping, so this is used to build default values
 * and the field-key-to-label map for error display.
 */
function flattenFieldDefinitions(formContent) {
  const result = [];
  for (const item of formContent) {
    if (item.type === "field") {
      result.push(item.definition);
    } else {
      for (const fieldDef of item.definition.fields) {
        result.push(fieldDef);
      }
    }
  }
  return result;
}
const SKELETON_FIELD_COUNT = 3;

// Mimics the label + input layout of real form fields.
const FORM_SKELETON = Array.from({
  length: SKELETON_FIELD_COUNT
}, (_, i) => /*#__PURE__*/React.createElement("div", {
  key: i,
  className: styles.osdkFormSkeletonField
}, /*#__PURE__*/React.createElement(SkeletonBar, {
  className: styles.osdkFormSkeletonLabel
}), /*#__PURE__*/React.createElement(SkeletonBar, {
  className: styles.osdkFormSkeletonInput
})));
function buildDefaultValues(fieldDefinitions) {
  const values = {};
  for (const def of fieldDefinitions) {
    const props = def.fieldComponentProps;
    if ("defaultValue" in props) {
      values[def.fieldKey] = props.defaultValue;
    }
  }
  return values;
}
const SubmitButton = /*#__PURE__*/memo(function ({
  isPending,
  isSubmitDisabled,
  errorMessage,
  buttonText,
  buttonVariant,
  onClick
}) {
  const buttonLabel = isPending ? "Submitting\u2026" : buttonText;
  const button = /*#__PURE__*/React.createElement(ActionButton, {
    type: "button",
    variant: buttonVariant,
    disabled: isSubmitDisabled || isPending,
    onClick: onClick
  }, buttonLabel);
  if (errorMessage == null) {
    return button;
  }
  return /*#__PURE__*/React.createElement(Tooltip.Root, {
    defaultOpen: true
  }, /*#__PURE__*/React.createElement(Tooltip.Trigger, {
    render: /*#__PURE__*/React.createElement("span", {
      className: styles.osdkTooltipTriggerWrapper
    })
  }, button), /*#__PURE__*/React.createElement(Tooltip.Portal, null, /*#__PURE__*/React.createElement(Tooltip.Positioner, null, /*#__PURE__*/React.createElement(Tooltip.Popup, null, /*#__PURE__*/React.createElement(Tooltip.Arrow, null), errorMessage))));
});
// memo omitted: errorEntries is always a new array (RHF reuses the same errors ref)
function ErrorIndicator({
  errorEntries
}) {
  if (errorEntries.length === 0) {
    return null;
  }
  const count = errorEntries.length;
  return /*#__PURE__*/React.createElement(Tooltip.Root, null, /*#__PURE__*/React.createElement(Tooltip.Trigger, null, /*#__PURE__*/React.createElement("span", {
    className: styles.osdkFormErrorIndicator
  }, /*#__PURE__*/React.createElement(ErrorIcon, {
    size: 14
  }), count === 1 ? "1 issue" : `${count} issues`)), /*#__PURE__*/React.createElement(Tooltip.Portal, null, /*#__PURE__*/React.createElement(Tooltip.Positioner, null, /*#__PURE__*/React.createElement(Tooltip.Popup, null, /*#__PURE__*/React.createElement(Tooltip.Arrow, null), /*#__PURE__*/React.createElement("ul", {
    className: styles.osdkFormErrorList
  }, errorEntries.map(entry => /*#__PURE__*/React.createElement("li", {
    key: entry.label
  }, /*#__PURE__*/React.createElement("strong", null, entry.label, ":"), " ", entry.message)))))));
}
//# sourceMappingURL=BaseForm.js.map