/**
 * Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these
 * node types is largely the same, with minor differences handled server-side. For the `query`
 * field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting.
 */
import { ErrorMessage, Form, Formik } from 'formik';
import NamespaceHeader from '../../components/NamespaceHeader';
import { useContext, useEffect, useState } from 'react';
import DJClientContext from '../../providers/djclient';
import 'styles/node-creation.scss';
import './styles.css';
import { useParams, useNavigate } from 'react-router-dom';
import { FullNameField } from './FullNameField';
import { MetricQueryField } from './MetricQueryField';
import { displayMessageAfterSubmit } from '../../../utils/form';
import { NodeQueryField } from './NodeQueryField';
import { MetricMetadataFields } from './MetricMetadataFields';
import { UpstreamNodeField } from './UpstreamNodeField';
import { OwnersField } from './OwnersField';
import { TagsField } from './TagsField';
import { NamespaceField } from './NamespaceField';
import { AlertMessage } from './AlertMessage';
import { DisplayNameField } from './DisplayNameField';
import { DescriptionField } from './DescriptionField';
import { NodeModeField } from './NodeModeField';
import { RequiredDimensionsSelect } from './RequiredDimensionsSelect';
import LoadingIcon from '../../icons/LoadingIcon';
import { ColumnsSelect } from './ColumnsSelect';
import { CustomMetadataField } from './CustomMetadataField';

class Action {
  static Add = new Action('add');
  static Edit = new Action('edit');

  constructor(name) {
    this.name = name;
  }
}

export function AddEditNodePage({ extensions = {} }) {
  const djClient = useContext(DJClientContext).DataJunctionAPI;
  const navigate = useNavigate();

  let { nodeType, initialNamespace, name } = useParams();
  const action = name !== undefined ? Action.Edit : Action.Add;

  // A read-only (git-managed) node can't be edited in the UI. If someone hits
  // the edit route directly, send them back to the node view once the
  // namespace's read-only verdict (from NamespaceHeader) is known.
  const [isReadOnly, setIsReadOnly] = useState(undefined);
  useEffect(() => {
    if (action === Action.Edit && isReadOnly === true) {
      navigate(`/nodes/${name}`, { replace: true });
    }
  }, [action, isReadOnly, name, navigate]);

  const headerNamespace = initialNamespace
    ? initialNamespace
    : name
    ? name.substring(0, name.lastIndexOf('.'))
    : '';

  const initialValues = {
    name: action === Action.Edit ? name : '',
    namespace: action === Action.Add ? initialNamespace : '',
    display_name: '',
    query: '',
    type: nodeType,
    description: '',
    primary_key: '',
    mode: 'published',
    owners: [],
    custom_metadata: '',
  };

  const validator = values => {
    const errors = {};
    // Display name drives the synthesized full name (`namespace.display_name`).
    // Without it the full name collapses to `namespace.` — non-empty but
    // useless. Validate the user-facing field instead of the derived one.
    if (action === Action.Add && !values.display_name?.trim()) {
      errors.display_name = 'Required';
    }
    if (
      !values.name ||
      values.name.endsWith('.') ||
      values.name.startsWith('.') ||
      values.name.includes('..')
    ) {
      errors.name = 'Required';
    }
    if (values.type !== 'metric' && !values.query) {
      errors.query = 'Required';
    }
    return errors;
  };

  const handleSubmit = async (values, { setSubmitting, setStatus }) => {
    if (action === Action.Add) {
      await createNode(values, setStatus).then(_ => {
        window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
        setSubmitting(false);
      });
    } else {
      await patchNode(values, setStatus).then(_ => {
        window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
        setSubmitting(false);
      });
    }
  };
  const submitHandlers = [handleSubmit];

  const pageTitle =
    action === Action.Add ? (
      <h2>
        Create{' '}
        <span className={`node_type__${nodeType} node_type_creation_heading`}>
          {nodeType}
        </span>
      </h2>
    ) : (
      <h2>Edit</h2>
    );

  const staticFieldsInEdit = node => (
    <>
      <div className="NodeNameInput NodeCreationInput">
        <label>Name</label> {name}
      </div>
      <div className="NodeNameInput NodeCreationInput">
        <label>Type</label> {node.type}
      </div>
    </>
  );

  const primaryKeyToList = primaryKey => {
    return primaryKey.map(columnName => columnName.trim());
  };

  const parseCustomMetadata = customMetadata => {
    if (!customMetadata || customMetadata.trim() === '') {
      return null;
    }
    try {
      return JSON.parse(customMetadata);
    } catch (err) {
      return null;
    }
  };

  /**
   * Build the metric query based on whether an upstream node is provided.
   * - With upstream node: `SELECT <expression> FROM <upstream_node>` (regular metric)
   * - Without upstream node: `SELECT <expression>` (derived metric referencing other metrics)
   */
  const buildMetricQuery = (aggregateExpression, upstreamNode) => {
    if (upstreamNode && upstreamNode.trim() !== '') {
      return `SELECT ${aggregateExpression} \n FROM ${upstreamNode}`;
    }
    // Derived metric - no FROM clause needed, expression references other metrics directly
    return `SELECT ${aggregateExpression}`;
  };

  const createNode = async (values, setStatus) => {
    const { status, json } = await djClient.createNode(
      nodeType,
      values.name,
      values.display_name,
      values.description,
      values.type === 'metric'
        ? buildMetricQuery(values.aggregate_expression, values.upstream_node)
        : values.query,
      values.mode,
      values.namespace,
      values.primary_key ? primaryKeyToList(values.primary_key) : null,
      values.metric_direction,
      values.metric_unit,
      // Only send required_dimensions if it's set (for metrics)
      values.required_dimensions && Array.isArray(values.required_dimensions)
        ? values.required_dimensions
        : undefined,
      parseCustomMetadata(values.custom_metadata),
    );
    if (status === 200 || status === 201) {
      if (values.tags) {
        await djClient.tagsNode(values.name, values.tags);
      }
      setStatus({
        success: (
          <>
            Successfully created {json.type} node{' '}
            <a href={`/nodes/${json.name}`}>{json.name}</a>!
          </>
        ),
      });
    } else {
      setStatus({
        failure: `${json.message}`,
      });
    }
  };

  const patchNode = async (values, setStatus) => {
    const { status, json } = await djClient.patchNode(
      values.name,
      values.display_name,
      values.description,
      values.type === 'metric'
        ? buildMetricQuery(values.aggregate_expression, values.upstream_node)
        : values.query,
      values.mode,
      values.primary_key ? primaryKeyToList(values.primary_key) : null,
      values.metric_direction,
      values.metric_unit,
      values.significant_digits,
      // Only send required_dimensions if it's set (for metrics)
      values.required_dimensions && Array.isArray(values.required_dimensions)
        ? values.required_dimensions
        : undefined,
      values.owners,
      parseCustomMetadata(values.custom_metadata),
    );
    const tagsResponse = await djClient.tagsNode(
      values.name,
      values.tags.map(tag => tag),
    );
    if ((status === 200 || status === 201) && tagsResponse.status === 200) {
      setStatus({
        success: (
          <>
            Successfully updated {json.type} node{' '}
            <a href={`/nodes/${json.name}`}>{json.name}</a>!
          </>
        ),
      });
    } else {
      setStatus({
        failure: `${json.message}`,
      });
    }
  };

  const fullNameInput = (
    <div className="FullNameInput NodeCreationInput">
      <ErrorMessage name="name" component="span" />
      <label htmlFor="FullName">Full Name *</label>
      <FullNameField type="text" name="name" />
    </div>
  );

  const nodeCanBeEdited = nodeType => {
    return new Set(['transform', 'metric', 'dimension']).has(nodeType);
  };

  const getExistingNodeData = async name => {
    const node = await djClient.getNodeForEditing(name);
    if (node === null) {
      return { message: `Node ${name} does not exist` };
    }
    const baseData = {
      name: node.name,
      type: node.type.toLowerCase(),
      display_name: node.current.displayName,
      description: node.current.description,
      primary_key: node.current.primaryKey,
      query: node.current.query,
      tags: node.tags,
      mode: node.current.mode.toLowerCase(),
      owners: node.owners,
      custom_metadata: node.current.customMetadata,
    };

    if (node.type === 'METRIC') {
      // Check if this is a derived metric (any parent is a metric)
      const isDerivedMetric = node.current.isDerivedMetric;
      // For regular metrics, get the first non-metric parent as upstream node
      const nonMetricParent = node.current.parents.find(
        p => p.type !== 'METRIC',
      );

      if (isDerivedMetric) {
        // Derived metric: no upstream node, expression is the full query projection
        // Parse the expression from the query (format: "SELECT <expression>")
        const query = node.current.query || '';
        const selectMatch = query.match(/SELECT\s+(.+)/is);
        const derivedExpression = selectMatch
          ? selectMatch[1].trim()
          : node.current.metricMetadata?.expression || '';

        return {
          ...baseData,
          metric_direction:
            node.current.metricMetadata?.direction?.toLowerCase(),
          metric_unit: node.current.metricMetadata?.unit?.name?.toLowerCase(),
          significant_digits: node.current.metricMetadata?.significantDigits,
          required_dimensions: node.current.requiredDimensions.map(
            dim => dim.name,
          ),
          upstream_node: '', // Derived metrics have no upstream node
          aggregate_expression: derivedExpression,
        };
      } else {
        // Regular metric: has upstream node (first non-metric parent)
        return {
          ...baseData,
          metric_direction:
            node.current.metricMetadata?.direction?.toLowerCase(),
          metric_unit: node.current.metricMetadata?.unit?.name?.toLowerCase(),
          significant_digits: node.current.metricMetadata?.significantDigits,
          required_dimensions: node.current.requiredDimensions.map(
            dim => dim.name,
          ),
          upstream_node: nonMetricParent?.name || '',
          aggregate_expression: node.current.metricMetadata?.expression,
        };
      }
    }
    return baseData;
  };

  const runValidityChecks = (data, setNode, setMessage) => {
    // Check if node exists
    if (data.message !== undefined) {
      setNode(null);
      setMessage(`Node ${name} does not exist!`);
      return;
    }
    // Check if node type can be edited
    if (!nodeCanBeEdited(data.type)) {
      setNode(null);
      if (data.type === 'cube') {
        navigate(`/nodes/${data.name}/edit-cube`);
      }
      setMessage(`Node ${name} is of type ${data.type} and cannot be edited`);
    }
  };

  const updateFieldsWithNodeData = (
    data,
    setFieldValue,
    setNode,
    setSelectTags,
    setSelectPrimaryKey,
    setSelectUpstreamNode,
    setSelectRequiredDims,
    setSelectOwners,
  ) => {
    // Update fields with existing data to prepare for edit
    const fields = [
      'display_name',
      'query',
      'type',
      'description',
      'primary_key',
      'mode',
      'tags',
      'aggregate_expression',
      'upstream_node',
      'metric_unit',
      'metric_direction',
      'significant_digits',
      'required_dimensions',
      'owners',
      'custom_metadata',
    ];
    fields.forEach(field => {
      if (field === 'tags') {
        setFieldValue(
          field,
          data[field].map(tag => tag.name),
        );
      } else if (field === 'owners') {
        setFieldValue(
          field,
          data[field].map(owner => owner.username),
        );
      } else if (field === 'custom_metadata') {
        const value = data[field] ? JSON.stringify(data[field], null, 2) : '';
        setFieldValue(field, value, false);
      } else {
        setFieldValue(field, data[field] || '', false);
      }
    });
    setNode(data);

    // For react-select fields, we have to explicitly set the entire
    // field rather than just the values
    setSelectTags(
      <TagsField
        defaultValue={data.tags.map(t => {
          return { value: t.name, label: t.displayName };
        })}
      />,
    );
    setSelectPrimaryKey(
      <ColumnsSelect
        defaultValue={data.primary_key}
        fieldName="primary_key"
        label="Primary Key"
        isMulti={true}
      />,
    );
    if (data.required_dimensions) {
      setSelectRequiredDims(
        <RequiredDimensionsSelect
          defaultValue={data.required_dimensions.map(dim => {
            return { value: dim, label: dim };
          })}
        />,
      );
    }
    // For derived metrics, upstream_node is empty - pass null to clear the select
    setSelectUpstreamNode(
      <UpstreamNodeField
        defaultValue={
          data.upstream_node
            ? { value: data.upstream_node, label: data.upstream_node }
            : null
        }
      />,
    );
    if (data.owners) {
      setSelectOwners(
        <OwnersField
          defaultValue={data.owners.map(owner => {
            return { value: owner.username, label: owner.username };
          })}
        />,
      );
    }
  };

  // Editing a node we can't yet confirm is editable (verdict still loading) or
  // one that is read-only: render just the header (which resolves the verdict
  // and triggers the redirect above) — never the editor form. Avoids flashing
  // the editor before redirecting a read-only node.
  if (action === Action.Edit && isReadOnly !== false) {
    return (
      <div className="mid">
        <NamespaceHeader
          namespace={headerNamespace}
          onReadOnlyChange={setIsReadOnly}
        />
      </div>
    );
  }

  return (
    <div className="mid">
      <NamespaceHeader
        namespace={headerNamespace}
        onReadOnlyChange={setIsReadOnly}
      />
      <div className="node-builder">
        <div className="node-builder-main">
          <Formik
            initialValues={initialValues}
            validate={validator}
            validateOnChange={true}
            validateOnBlur={true}
            onSubmit={async (values, { setSubmitting, setStatus }) => {
              try {
                for (const handler of submitHandlers) {
                  await handler(values, { setSubmitting, setStatus });
                }
              } catch (error) {
                console.error('Error in submission', error);
              } finally {
                setSubmitting(false);
              }
            }}
          >
            {function Render(formikProps) {
              const {
                isSubmitting,
                status,
                setFieldValue,
                errors,
                touched,
                isValid,
                dirty,
              } = formikProps;
              const [node, setNode] = useState([]);
              const [selectPrimaryKey, setSelectPrimaryKey] = useState(null);
              const [selectRequiredDims, setSelectRequiredDims] =
                useState(null);
              const [selectUpstreamNode, setSelectUpstreamNode] =
                useState(null);
              const [selectOwners, setSelectOwners] = useState(null);
              const [selectTags, setSelectTags] = useState(null);
              const [message, setMessage] = useState('');

              useEffect(() => {
                const fetchData = async () => {
                  if (action === Action.Edit) {
                    const data = await getExistingNodeData(name);
                    runValidityChecks(data, setNode, setMessage);
                    updateFieldsWithNodeData(
                      data,
                      setFieldValue,
                      setNode,
                      setSelectTags,
                      setSelectPrimaryKey,
                      setSelectUpstreamNode,
                      setSelectRequiredDims,
                      setSelectOwners,
                    );
                  }
                };
                fetchData().catch(console.error);
              }, [setFieldValue]);
              return (
                <Form>
                  {displayMessageAfterSubmit(status)}
                  {action === Action.Edit && message ? (
                    <AlertMessage message={message} />
                  ) : (
                    <>
                      <div className="node-builder-form-header">
                        {pageTitle}
                        <NodeModeField />
                      </div>

                      <div className="node-group">
                        <div className="node-row">
                          {action === Action.Add ? (
                            <NamespaceField
                              initialNamespace={initialNamespace}
                            />
                          ) : (
                            staticFieldsInEdit(node)
                          )}
                          <DisplayNameField />
                        </div>
                        {action === Action.Add ? fullNameInput : ''}
                        <DescriptionField />
                      </div>

                      <div className="node-group">
                        {nodeType === 'metric' || node?.type === 'metric' ? (
                          action === Action.Edit ? (
                            selectUpstreamNode
                          ) : (
                            <UpstreamNodeField />
                          )
                        ) : null}
                        {nodeType === 'metric' || node.type === 'metric' ? (
                          <MetricQueryField
                            djClient={djClient}
                            value={
                              node.aggregate_expression
                                ? node.aggregate_expression
                                : ''
                            }
                          />
                        ) : (
                          <NodeQueryField
                            djClient={djClient}
                            value={node.query ? node.query : ''}
                          />
                        )}
                        {(nodeType === 'metric' || node.type === 'metric') && (
                          <MetricMetadataFields />
                        )}
                      </div>

                      <div className="node-group">
                        {nodeType !== 'metric' && node.type !== 'metric' ? (
                          action === Action.Edit ? (
                            selectPrimaryKey
                          ) : (
                            <ColumnsSelect
                              defaultValue={[]}
                              fieldName="primary_key"
                              label="Primary Key"
                              isMulti={true}
                            />
                          )
                        ) : action === Action.Edit ? (
                          selectRequiredDims
                        ) : (
                          <RequiredDimensionsSelect />
                        )}
                      </div>

                      <div className="node-group">
                        <div className="node-row">
                          {action === Action.Edit ? (
                            selectOwners
                          ) : (
                            <OwnersField />
                          )}
                          {action === Action.Edit ? selectTags : <TagsField />}
                        </div>
                      </div>

                      {Object.entries(extensions).map(
                        ([key, ExtensionComponent]) => (
                          <div key={key} className="node-group">
                            <ExtensionComponent
                              node={node}
                              action={action}
                              registerSubmitHandler={(
                                onSubmit,
                                { prepend } = {},
                              ) => {
                                if (!submitHandlers.includes(onSubmit)) {
                                  if (prepend) {
                                    submitHandlers.unshift(onSubmit);
                                  } else {
                                    submitHandlers.push(onSubmit);
                                  }
                                }
                              }}
                            />
                          </div>
                        ),
                      )}

                      <div className="node-group node-group-last">
                        <CustomMetadataField
                          value={
                            node.custom_metadata ? node.custom_metadata : ''
                          }
                        />
                      </div>

                      <button
                        type="submit"
                        disabled={isSubmitting || !isValid}
                        className={`save-node-btn${
                          isSubmitting ? ' save-node-btn--loading' : ''
                        }`}
                      >
                        {isSubmitting ? (
                          <>
                            <LoadingIcon />
                            Saving...
                          </>
                        ) : (
                          (action === Action.Add ? 'Create ' : 'Save ') +
                          (nodeType ? nodeType : '')
                        )}
                      </button>
                    </>
                  )}
                </Form>
              );
            }}
          </Formik>
        </div>
      </div>
    </div>
  );
}
