/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 { useState, useEffect, useCallback, useRef } from 'react';
import { t } from '@apache-superset/core/translation';
import { styled } from '@apache-superset/core/theme';
import { SupersetClient } from '@superset-ui/core';
import { Spin } from 'antd';
import { Select } from '@superset-ui/core/components';
import { Icons } from '@superset-ui/core/components/Icons';
import { JsonForms } from '@jsonforms/react';
import type { JsonSchema, UISchemaElement } from '@jsonforms/core';
import { cellRegistryEntries } from '@great-expectations/jsonforms-antd-renderers';
import type { ErrorObject } from 'ajv';
import {
  StandardModal,
  ModalFormField,
  MODAL_STANDARD_WIDTH,
} from 'src/components/Modal';
import {
  renderers,
  sanitizeSchema,
  buildUiSchema,
  getDynamicDependencies,
  areDependenciesSatisfied,
  serializeDependencyValues,
  SCHEMA_REFRESH_DEBOUNCE_MS,
} from 'src/features/semanticLayers/jsonFormsHelpers';

interface SemanticLayerOption {
  uuid: string;
  name: string;
}

interface AvailableView {
  name: string;
  already_added: boolean;
}

const ModalContent = styled.div`
  padding: ${({ theme }) => theme.sizeUnit * 4}px;
`;

const LoadingContainer = styled.div`
  display: flex;
  justify-content: center;
  padding: ${({ theme }) => theme.sizeUnit * 4}px;
`;

const SectionLabel = styled.div`
  color: ${({ theme }) => theme.colorText};
  font-size: ${({ theme }) => theme.fontSize}px;
  margin-top: ${({ theme }) => theme.sizeUnit}px;
  margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px;
`;

const VerticalFormFields = styled.div`
  margin-bottom: ${({ theme }) => theme.sizeUnit * 4}px;

  /* The antd renderer's VerticalLayout creates its own <Form> —
     force flex-column so gap controls spacing between fields */
  && form {
    display: flex;
    flex-direction: column;
    gap: ${({ theme }) => theme.sizeUnit * 4}px;
  }

  /* Reset antd default margins so gap controls all spacing */
  && .ant-form-item {
    margin-bottom: 0;
  }

  /* Override ant-form-item-horizontal: stack label above control */
  && .ant-form-item-row {
    flex-direction: column;
    align-items: stretch;
  }

  && .ant-form-item-label {
    text-align: left;
    max-width: 100%;
    flex: none;
    padding-bottom: ${({ theme }) => theme.sizeUnit}px;
  }

  && .ant-form-item-control {
    max-width: 100%;
    flex: auto;
  }

  && .ant-form-item-label > label {
    color: ${({ theme }) => theme.colorText};
    font-size: ${({ theme }) => theme.fontSize}px;
  }
`;

interface AddSemanticViewModalProps {
  show: boolean;
  onHide: () => void;
  onSuccess: () => void;
  addDangerToast: (msg: string) => void;
  addSuccessToast: (msg: string) => void;
}

export default function AddSemanticViewModal({
  show,
  onHide,
  onSuccess,
  addDangerToast,
  addSuccessToast,
}: AddSemanticViewModalProps) {
  // --- Layer ---
  const [layers, setLayers] = useState<SemanticLayerOption[]>([]);
  const [selectedLayerUuid, setSelectedLayerUuid] = useState<string | null>(
    null,
  );
  const [loadingLayers, setLoadingLayers] = useState(false);

  // --- Runtime config ---
  const [runtimeSchema, setRuntimeSchema] = useState<JsonSchema | null>(null);
  const [runtimeUiSchema, setRuntimeUiSchema] = useState<
    UISchemaElement | undefined
  >();
  const [runtimeData, setRuntimeData] = useState<Record<string, unknown>>({});
  const [loadingRuntime, setLoadingRuntime] = useState(false);
  const [refreshingSchema, setRefreshingSchema] = useState(false);
  const errorsRef = useRef<ErrorObject[]>([]);
  const dynamicDepsRef = useRef<Record<string, string[]>>({});
  const lastDepSnapshotRef = useRef('');
  const schemaTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // --- Views ---
  const [availableViews, setAvailableViews] = useState<AvailableView[]>([]);
  const [selectedViewNames, setSelectedViewNames] = useState<string[]>([]);
  const [loadingViews, setLoadingViews] = useState(false);
  const viewsTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const lastViewsKeyRef = useRef('');

  // --- Misc ---
  const [saving, setSaving] = useState(false);
  const fetchGenRef = useRef(0);

  // =========================================================================
  // Fetch helpers
  // =========================================================================

  const fetchLayers = async () => {
    setLoadingLayers(true);
    try {
      const { json } = await SupersetClient.get({
        endpoint: '/api/v1/semantic_layer/',
      });
      setLayers(
        (json.result ?? []).map((l: { uuid: string; name: string }) => ({
          uuid: l.uuid,
          name: l.name,
        })),
      );
    } catch {
      addDangerToast(t('An error occurred while fetching semantic layers'));
    } finally {
      setLoadingLayers(false);
    }
  };

  const fetchViews = useCallback(
    async (uuid: string, rData: Record<string, unknown>, gen: number) => {
      setLoadingViews(true);
      setAvailableViews([]);
      setSelectedViewNames([]);
      try {
        const { json } = await SupersetClient.post({
          endpoint: `/api/v1/semantic_layer/${uuid}/views`,
          jsonPayload: { runtime_data: rData },
        });
        if (gen !== fetchGenRef.current) return;
        setAvailableViews(json.result ?? []);
      } catch {
        if (gen !== fetchGenRef.current) return;
        addDangerToast(t('An error occurred while fetching available views'));
      } finally {
        if (gen === fetchGenRef.current) setLoadingViews(false);
      }
    },
    [addDangerToast],
  );

  const applyRuntimeSchema = useCallback((rawSchema: JsonSchema) => {
    const schema = sanitizeSchema(rawSchema);
    setRuntimeSchema(schema);
    setRuntimeUiSchema(buildUiSchema(schema));
    dynamicDepsRef.current = getDynamicDependencies(rawSchema);
  }, []);

  const scheduleFetchViews = useCallback(
    (uuid: string, data: Record<string, unknown>) => {
      const key = JSON.stringify(data);
      if (key === lastViewsKeyRef.current) return;
      lastViewsKeyRef.current = key;
      if (viewsTimerRef.current) clearTimeout(viewsTimerRef.current);
      viewsTimerRef.current = setTimeout(() => {
        fetchViews(uuid, data, fetchGenRef.current);
      }, SCHEMA_REFRESH_DEBOUNCE_MS);
    },
    [fetchViews],
  );

  // =========================================================================
  // Layer change — fetch runtime schema, clear downstream state
  // =========================================================================

  const handleLayerChange = useCallback(
    async (uuid: string) => {
      fetchGenRef.current += 1;
      const gen = fetchGenRef.current;

      setSelectedLayerUuid(uuid);
      if (schemaTimerRef.current) clearTimeout(schemaTimerRef.current);
      if (viewsTimerRef.current) clearTimeout(viewsTimerRef.current);
      setRuntimeSchema(null);
      setRuntimeUiSchema(undefined);
      setRuntimeData({});
      errorsRef.current = [];
      dynamicDepsRef.current = {};
      lastDepSnapshotRef.current = '';
      setAvailableViews([]);
      setSelectedViewNames([]);
      lastViewsKeyRef.current = '';

      setLoadingRuntime(true);
      try {
        const { json } = await SupersetClient.post({
          endpoint: `/api/v1/semantic_layer/${uuid}/schema/runtime`,
          jsonPayload: {},
        });
        if (gen !== fetchGenRef.current) return;
        const schema = json.result;
        if (
          !schema?.properties ||
          Object.keys(schema.properties).length === 0
        ) {
          // No runtime config needed — fetch views right away
          fetchViews(uuid, {}, gen);
        } else {
          applyRuntimeSchema(schema);
        }
      } catch {
        if (gen !== fetchGenRef.current) return;
        addDangerToast(
          t('An error occurred while fetching the runtime schema'),
        );
      } finally {
        if (gen === fetchGenRef.current) setLoadingRuntime(false);
      }
    },
    [applyRuntimeSchema, fetchViews, addDangerToast],
  );

  // =========================================================================
  // Runtime form change — refresh dynamic fields or auto-fetch views
  // =========================================================================

  const handleRuntimeFormChange = useCallback(
    ({
      data,
      errors,
    }: {
      data: Record<string, unknown>;
      errors?: ErrorObject[];
    }) => {
      setRuntimeData(data);
      errorsRef.current = errors ?? [];

      if (!selectedLayerUuid) return;
      const gen = fetchGenRef.current;

      // Dynamic deps changed → refresh schema (e.g. database → schema)
      const dynamicDeps = dynamicDepsRef.current;
      if (Object.keys(dynamicDeps).length > 0) {
        const hasSatisfiedDeps = Object.values(dynamicDeps).some(deps =>
          areDependenciesSatisfied(deps, data),
        );
        if (hasSatisfiedDeps) {
          const snapshot = serializeDependencyValues(dynamicDeps, data);
          if (snapshot !== lastDepSnapshotRef.current) {
            lastDepSnapshotRef.current = snapshot;
            // Config is changing — clear views
            setAvailableViews([]);
            setSelectedViewNames([]);
            lastViewsKeyRef.current = '';
            if (schemaTimerRef.current) clearTimeout(schemaTimerRef.current);
            const uuid = selectedLayerUuid;
            schemaTimerRef.current = setTimeout(async () => {
              setRefreshingSchema(true);
              try {
                const { json } = await SupersetClient.post({
                  endpoint: `/api/v1/semantic_layer/${uuid}/schema/runtime`,
                  jsonPayload: { runtime_data: data },
                });
                if (gen !== fetchGenRef.current) return;
                applyRuntimeSchema(json.result);
              } catch {
                // Silent fail on refresh — form still works
              } finally {
                if (gen === fetchGenRef.current) setRefreshingSchema(false);
              }
            }, SCHEMA_REFRESH_DEBOUNCE_MS);
            return;
          }
        }
      }

      // No schema refresh needed — fetch views if form is valid
      if (errorsRef.current.length === 0) {
        scheduleFetchViews(selectedLayerUuid, data);
      }
    },
    [selectedLayerUuid, applyRuntimeSchema, scheduleFetchViews],
  );

  // After a schema refresh settles, JSON Forms re-validates and fires
  // onChange → handleRuntimeFormChange handles view fetching. As a fallback
  // (in case onChange doesn't fire), try once refreshingSchema flips false.
  const prevRefreshingRef = useRef(false);
  useEffect(() => {
    if (prevRefreshingRef.current && !refreshingSchema && selectedLayerUuid) {
      const timer = setTimeout(() => {
        if (errorsRef.current.length === 0) {
          scheduleFetchViews(selectedLayerUuid, runtimeData);
        }
      }, 100);
      prevRefreshingRef.current = false;
      return () => clearTimeout(timer);
    }
    prevRefreshingRef.current = refreshingSchema;
    return undefined;
  }, [refreshingSchema, selectedLayerUuid, runtimeData, scheduleFetchViews]);

  // =========================================================================
  // Modal open / close
  // =========================================================================

  useEffect(() => {
    if (show) {
      fetchLayers();
    } else {
      fetchGenRef.current += 1;
      if (schemaTimerRef.current) clearTimeout(schemaTimerRef.current);
      if (viewsTimerRef.current) clearTimeout(viewsTimerRef.current);
      setLayers([]);
      setSelectedLayerUuid(null);
      setLoadingLayers(false);
      setRuntimeSchema(null);
      setRuntimeUiSchema(undefined);
      setRuntimeData({});
      setLoadingRuntime(false);
      setRefreshingSchema(false);
      errorsRef.current = [];
      dynamicDepsRef.current = {};
      lastDepSnapshotRef.current = '';
      setAvailableViews([]);
      setSelectedViewNames([]);
      setLoadingViews(false);
      setSaving(false);
      lastViewsKeyRef.current = '';
    }
  }, [show]); // eslint-disable-line react-hooks/exhaustive-deps

  // =========================================================================
  // Save
  // =========================================================================

  const newViewCount = availableViews.filter(
    v => selectedViewNames.includes(v.name) && !v.already_added,
  ).length;

  const handleSave = async () => {
    if (!selectedLayerUuid || newViewCount === 0) return;
    setSaving(true);
    try {
      const viewsToCreate = availableViews
        .filter(v => selectedViewNames.includes(v.name) && !v.already_added)
        .map(v => ({
          name: v.name,
          semantic_layer_uuid: selectedLayerUuid,
          configuration: runtimeData,
        }));

      const { json } = await SupersetClient.post({
        endpoint: '/api/v1/semantic_view/',
        jsonPayload: { views: viewsToCreate },
      });

      const created = Array.isArray(json?.result?.created)
        ? json.result.created
        : [];
      const errors = Array.isArray(json?.result?.errors)
        ? json.result.errors
        : [];

      if (created.length > 0) {
        addSuccessToast(t('%s semantic view(s) added', created.length));
      }

      if (errors.length > 0) {
        const failedNames = errors
          .map((error: { name?: string }) => error?.name)
          .filter((name: string | undefined): name is string => !!name);
        addDangerToast(
          failedNames.length > 0
            ? t(
                '%s semantic view(s) failed to add: %s',
                errors.length,
                failedNames.join(', '),
              )
            : t('%s semantic view(s) failed to add', errors.length),
        );
      }

      if (created.length > 0 && errors.length === 0) {
        onSuccess();
        onHide();
      } else if (errors.length === 0) {
        addDangerToast(t('An error occurred while adding semantic views'));
      }
    } catch {
      addDangerToast(t('An error occurred while adding semantic views'));
    } finally {
      setSaving(false);
    }
  };

  // =========================================================================
  // Render
  // =========================================================================

  const hasRuntimeFields =
    runtimeSchema?.properties &&
    Object.keys(runtimeSchema.properties).length > 0;

  const viewsDisabled =
    loadingViews || (!loadingViews && availableViews.length === 0);

  // When ``x-singleView: true`` the runtime form fully describes a single
  // semantic view (e.g. a MetricFlow cube).  Hide the picker and auto-select
  // whatever ``get_semantic_views`` returned so the Add button can fire
  // without an extra user click.
  const singleViewMode =
    (runtimeSchema as Record<string, unknown> | null)?.['x-singleView'] ===
    true;

  useEffect(() => {
    if (!singleViewMode) return;
    const namesToAdd = availableViews
      .filter(v => !v.already_added)
      .map(v => v.name);
    setSelectedViewNames(prev => {
      if (
        prev.length === namesToAdd.length &&
        prev.every((n, i) => n === namesToAdd[i])
      ) {
        return prev;
      }
      return namesToAdd;
    });
  }, [singleViewMode, availableViews]);

  return (
    <StandardModal
      show={show}
      onHide={onHide}
      onSave={handleSave}
      title={t('Add Semantic View')}
      icon={<Icons.PlusOutlined />}
      width={MODAL_STANDARD_WIDTH}
      saveDisabled={newViewCount === 0 || saving}
      saveText={newViewCount > 0 ? t('Add %s view(s)', newViewCount) : t('Add')}
      saveLoading={saving}
    >
      <ModalContent>
        {/* Semantic Layer */}
        <ModalFormField label={t('Semantic Layer')}>
          <Select
            ariaLabel={t('Semantic layer')}
            placeholder={t('Select a semantic layer')}
            loading={loadingLayers}
            value={selectedLayerUuid}
            onChange={value => handleLayerChange(value as string)}
            options={layers.map(l => ({
              value: l.uuid,
              label: l.name,
            }))}
            getPopupContainer={() => document.body}
          />
        </ModalFormField>

        {/* Loading runtime schema */}
        {loadingRuntime && (
          <LoadingContainer>
            <Spin size="small" />
          </LoadingContainer>
        )}

        {/* Source location (runtime config fields) */}
        {hasRuntimeFields && !loadingRuntime && (
          <>
            <SectionLabel>{t('Source location')}</SectionLabel>
            <VerticalFormFields>
              <JsonForms
                schema={runtimeSchema!}
                uischema={runtimeUiSchema}
                data={runtimeData}
                renderers={renderers}
                cells={cellRegistryEntries}
                config={{ refreshingSchema, formData: runtimeData }}
                validationMode="ValidateAndHide"
                onChange={handleRuntimeFormChange}
              />
            </VerticalFormFields>
          </>
        )}

        {/* Semantic Views — always visible once a layer is selected, unless
            the runtime schema declares ``x-singleView: true``: extensions
            (e.g. MetricFlow cubes) whose runtime form fully describes a
            single view set that flag so the picker disappears and the
            view is auto-selected when ``get_semantic_views`` returns it. */}
        {selectedLayerUuid && !loadingRuntime && !singleViewMode && (
          <ModalFormField label={t('Semantic Views')}>
            <Select
              ariaLabel={t('Semantic views')}
              placeholder={t('Select semantic views')}
              mode="multiple"
              loading={loadingViews}
              disabled={viewsDisabled}
              value={selectedViewNames}
              onChange={values => setSelectedViewNames(values as string[])}
              options={availableViews
                .sort((a, b) => a.name.localeCompare(b.name))
                .map(v => ({
                  value: v.name,
                  label: v.already_added
                    ? `${v.name} (${t('already added')})`
                    : v.name,
                  disabled: v.already_added,
                }))}
              getPopupContainer={() => document.body}
            />
          </ModalFormField>
        )}
      </ModalContent>
    </StandardModal>
  );
}
