'use client';

import fileDownload from 'js-file-download';
import { createContext, useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';

import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
import { useSetEditors } from '@/backend/library/useSetEditors';
import { useSetLocation } from '@/backend/library/useSetLocation';
import { useSetOwner } from '@/backend/library/useSetOwner';
import { useFindPredecessor } from '@/backend/oss/useFindPredecessor';
import { ICstCreateDTO, ICstRenameDTO, ICstUpdateDTO, IInlineSynthesisDTO } from '@/backend/rsform/api';
import { useCstCreate } from '@/backend/rsform/useCstCreate';
import { useCstDelete } from '@/backend/rsform/useCstDelete';
import { useCstMove } from '@/backend/rsform/useCstMove';
import { useCstRename } from '@/backend/rsform/useCstRename';
import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute';
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm';
import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { useProduceStructure } from '@/backend/rsform/useProduceStructure';
import { useResetAliases } from '@/backend/rsform/useResetAliases';
import { useRestoreOrder } from '@/backend/rsform/useRestoreOrder';
import { useRSFormSuspense } from '@/backend/rsform/useRSForm';
import {
  AccessPolicy,
  ILibraryItemEditor,
  IVersionData,
  LibraryItemID,
  LocationHead,
  VersionID
} from '@/models/library';
import { ICstSubstitutions } from '@/models/oss';
import { ConstituentaID, CstType, IConstituenta, IConstituentaMeta, IRSForm, TermForm } from '@/models/rsform';
import { generateAlias } from '@/models/rsformAPI';
import { UserID, UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences';
import { useRoleStore } from '@/stores/role';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';

import { RSTabID } from './RSTabs';

export interface IRSEditContext extends ILibraryItemEditor {
  schema?: IRSForm;
  selected: ConstituentaID[];

  isOwned: boolean;
  isArchive: boolean;
  isMutable: boolean;
  isContentEditable: boolean;
  isProcessing: boolean;
  isAttachedToOSS: boolean;
  canProduceStructure: boolean;
  canDeleteSelected: boolean;

  setOwner: (newOwner: UserID) => void;
  setAccessPolicy: (newPolicy: AccessPolicy) => void;
  promptEditors: () => void;
  promptLocation: () => void;

  setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
  select: (target: ConstituentaID) => void;
  deselect: (target: ConstituentaID) => void;
  toggleSelect: (target: ConstituentaID) => void;
  deselectAll: () => void;

  viewOSS: (target: LibraryItemID, newTab?: boolean) => void;
  viewVersion: (version?: VersionID, newTab?: boolean) => void;
  viewPredecessor: (target: ConstituentaID) => void;
  createVersion: () => void;
  restoreVersion: () => void;
  promptEditVersions: () => void;

  moveUp: () => void;
  moveDown: () => void;
  createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
  renameCst: () => void;
  cloneCst: () => void;
  promptDeleteCst: () => void;
  editTermForms: () => void;

  promptTemplate: () => void;
  promptClone: () => void;
  promptUpload: () => void;
  share: () => void;
  download: () => void;

  reindex: () => void;
  reorder: () => void;
  produceStructure: () => void;
  inlineSynthesis: () => void;
  substitute: () => void;

  showTypeGraph: () => void;
  showQR: () => void;
}

const RSEditContext = createContext<IRSEditContext | null>(null);
export const useRSEdit = () => {
  const context = useContext(RSEditContext);
  if (context === null) {
    throw new Error('useRSEdit has to be used within <RSEditState>');
  }
  return context;
};

interface RSEditStateProps {
  itemID: LibraryItemID;
  versionID?: VersionID;

  selected: ConstituentaID[];
  isModified: boolean;
  setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
  activeCst?: IConstituenta;

  onCreateCst?: (newCst: IConstituentaMeta) => void;
  onDeleteCst?: (newActive?: ConstituentaID) => void;
}

export const RSEditState = ({
  itemID,
  versionID,
  selected,
  setSelected,
  activeCst,
  isModified,
  onCreateCst,
  onDeleteCst,
  children
}: React.PropsWithChildren<RSEditStateProps>) => {
  const router = useConceptNavigation();
  const { user } = useAuth();
  const adminMode = usePreferencesStore(state => state.adminMode);
  const role = useRoleStore(state => state.role);
  const adjustRole = useRoleStore(state => state.adjustRole);

  const { schema } = useRSFormSuspense({ itemID: itemID, version: versionID });
  const isProcessing = useIsProcessingRSForm();

  const { download: downloadFile } = useDownloadRSForm();
  const { findPredecessor } = useFindPredecessor();

  const { setOwner: setItemOwner } = useSetOwner();
  const { setLocation: setItemLocation } = useSetLocation();
  const { setAccessPolicy: setItemAccessPolicy } = useSetAccessPolicy();
  const { setEditors: setItemEditors } = useSetEditors();

  const { cstCreate } = useCstCreate();
  const { cstRename } = useCstRename();
  const { cstSubstitute } = useCstSubstitute();
  const { cstMove } = useCstMove();
  const { cstDelete } = useCstDelete();
  const { cstUpdate } = useCstUpdate();

  const { produceStructure: produceStructureInternal } = useProduceStructure();
  const { inlineSynthesis: inlineSynthesisInternal } = useInlineSynthesis();
  const { restoreOrder: restoreOrderInternal } = useRestoreOrder();
  const { resetAliases: resetAliasesInternal } = useResetAliases();

  const isOwned = user?.id === schema?.owner || false;

  const isArchive = !!versionID;

  const isMutable = role > UserRole.READER && !schema?.read_only;
  const isContentEditable = isMutable && !isArchive;
  const canDeleteSelected = selected.length > 0 && selected.every(id => !schema?.cstByID.get(id)?.is_inherited);
  const isAttachedToOSS =
    !!schema && schema.oss.length > 0 && (schema.stats.count_inherited > 0 || schema.items.length === 0);

  const [renameInitialData, setRenameInitialData] = useState<ICstRenameDTO>();

  const showClone = useDialogsStore(state => state.showCloneLibraryItem);
  const showCreateVersion = useDialogsStore(state => state.showCreateVersion);
  const showEditVersions = useDialogsStore(state => state.showEditVersions);
  const showEditEditors = useDialogsStore(state => state.showEditEditors);
  const showEditLocation = useDialogsStore(state => state.showChangeLocation);

  const showCreateCst = useDialogsStore(state => state.showCreateCst);
  const showDeleteCst = useDialogsStore(state => state.showDeleteCst);

  const showRenameCst = useDialogsStore(state => state.showRenameCst);
  const showEditTerm = useDialogsStore(state => state.showEditWordForms);

  const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
  const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
  const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
  const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
  const showUpload = useDialogsStore(state => state.showUploadRSForm);
  const showQR = useDialogsStore(state => state.showQR);

  const typeInfo = schema
    ? schema.items.map(item => ({
        alias: item.alias,
        result: item.parse.typification,
        args: item.parse.args
      }))
    : [];

  const canProduceStructure =
    !!activeCst &&
    !!activeCst.parse.typification &&
    activeCst.cst_type !== CstType.BASE &&
    activeCst.cst_type !== CstType.CONSTANT;

  useEffect(
    () =>
      adjustRole({
        isOwner: isOwned,
        isEditor: (user && schema?.editors.includes(user?.id)) ?? false,
        isStaff: user?.is_staff ?? false,
        adminMode: adminMode
      }),
    [schema, adjustRole, isOwned, user, adminMode]
  );

  function viewVersion(version?: VersionID, newTab?: boolean) {
    router.push(urls.schema(itemID, version), newTab);
  }

  function viewPredecessor(target: ConstituentaID) {
    findPredecessor({ target: target }, reference =>
      router.push(
        urls.schema_props({
          id: reference.schema,
          active: reference.id,
          tab: RSTabID.CST_EDIT
        })
      )
    );
  }

  function viewOSS(target: LibraryItemID, newTab?: boolean) {
    router.push(urls.oss(target), newTab);
  }

  function restoreVersion() {
    if (!versionID || !window.confirm(prompts.restoreArchive)) {
      return;
    }
    model.versionRestore(versionID, () => {
      toast.success(information.versionRestored);
      viewVersion(undefined);
    });
  }

  function calculateCloneLocation() {
    if (!schema) {
      return LocationHead.USER;
    }
    const location = schema.location;
    const head = schema.location.substring(0, 2) as LocationHead;
    if (head === LocationHead.LIBRARY) {
      return user?.is_staff ? location : LocationHead.USER;
    }
    if (schema.owner === user?.id) {
      return location;
    }
    return head === LocationHead.USER ? LocationHead.USER : location;
  }

  function handleCreateCst(data: ICstCreateDTO) {
    if (!schema) {
      return;
    }
    data.alias = data.alias || generateAlias(data.cst_type, schema);
    cstCreate({ itemID: itemID, data }, newCst => {
      toast.success(information.newConstituent(newCst.alias));
      setSelected([newCst.id]);
      if (onCreateCst) onCreateCst(newCst);
    });
  }

  function handleRenameCst(data: ICstRenameDTO) {
    const oldAlias = renameInitialData?.alias ?? '';
    cstRename({ itemID: itemID, data }, () => toast.success(information.renameComplete(oldAlias, data.alias)));
  }

  function handleSubstituteCst(data: ICstSubstitutions) {
    cstSubstitute({ itemID: itemID, data }, () => {
      setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)));
      toast.success(information.substituteSingle);
    });
  }

  function handleDeleteCst(deleted: ConstituentaID[]) {
    if (!schema) {
      return;
    }
    const data = {
      items: deleted
    };

    const deletedNames = deleted.map(id => schema.cstByID.get(id)!.alias).join(', ');
    const isEmpty = deleted.length === schema.items.length;
    const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, schema.items, deleted);

    cstDelete({ itemID: itemID, data }, () => {
      toast.success(information.constituentsDestroyed(deletedNames));
      setSelected(nextActive ? [nextActive] : []);
      onDeleteCst?.(nextActive);
    });
  }

  function handleSaveWordforms(forms: TermForm[]) {
    if (!activeCst) {
      return;
    }
    const data: ICstUpdateDTO = {
      target: activeCst.id,
      item_data: { term_forms: forms }
    };
    cstUpdate({ itemID: itemID, data }, () => toast.success(information.changesSaved));
  }

  function handleCreateVersion(data: IVersionData) {
    if (!schema) {
      return;
    }
    model.versionCreate(data, () => {
      toast.success(information.newVersion(data.version));
    });
  }

  function handleDeleteVersion(versionID: VersionID) {
    if (!schema) {
      return;
    }
    model.versionDelete(versionID, () => {
      toast.success(information.versionDestroyed);
      if (versionID === versionID) {
        viewVersion(undefined);
      }
    });
  }

  function handleUpdateVersion(versionID: VersionID, data: IVersionData) {
    if (!schema) {
      return;
    }
    model.versionUpdate(versionID, data, () => toast.success(information.changesSaved));
  }

  const handleSetLocation = (newLocation: string) =>
    setItemLocation({ itemID: itemID, location: newLocation }, () => toast.success(information.moveComplete));

  function handleInlineSynthesis(data: IInlineSynthesisDTO) {
    if (!schema) {
      return;
    }
    const oldCount = schema.items.length;
    inlineSynthesisInternal({ itemID: itemID, data }, newSchema => {
      setSelected([]);
      toast.success(information.addedConstituents(newSchema.items.length - oldCount));
    });
  }

  function createVersion() {
    if (!schema || (isModified && !promptUnsaved())) {
      return;
    }
    showCreateVersion({
      versions: schema.versions,
      onCreate: handleCreateVersion,
      selected: selected,
      totalCount: schema.items.length
    });
  }

  function promptEditVersions() {
    if (!schema) {
      return;
    }
    showEditVersions({
      versions: schema.versions,
      onDelete: handleDeleteVersion,
      onUpdate: handleUpdateVersion
    });
  }

  function moveUp() {
    if (!schema?.items || selected.length === 0) {
      return;
    }
    const currentIndex = schema.items.reduce((prev, cst, index) => {
      if (!selected.includes(cst.id)) {
        return prev;
      } else if (prev === -1) {
        return index;
      }
      return Math.min(prev, index);
    }, -1);
    const target = Math.max(0, currentIndex - 1);
    cstMove({
      itemID: itemID,
      data: {
        items: selected,
        move_to: target
      }
    });
  }

  function moveDown() {
    if (!schema?.items || selected.length === 0) {
      return;
    }
    let count = 0;
    const currentIndex = schema.items.reduce((prev, cst, index) => {
      if (!selected.includes(cst.id)) {
        return prev;
      } else {
        count += 1;
        if (prev === -1) {
          return index;
        }
        return Math.max(prev, index);
      }
    }, -1);
    const target = Math.min(schema.items.length - 1, currentIndex - count + 2);
    cstMove({
      itemID: itemID,
      data: {
        items: selected,
        move_to: target
      }
    });
  }

  function createCst(type: CstType | undefined, skipDialog: boolean, definition?: string) {
    if (!schema) {
      return;
    }
    const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
    const data: ICstCreateDTO = {
      insert_after: activeCst?.id ?? null,
      cst_type: targetType,
      alias: generateAlias(targetType, schema),
      term_raw: '',
      definition_formal: definition ?? '',
      definition_raw: '',
      convention: '',
      term_forms: []
    };
    if (skipDialog) {
      handleCreateCst(data);
    } else {
      showCreateCst({ schema: schema, onCreate: handleCreateCst, initial: data });
    }
  }

  function cloneCst() {
    if (!activeCst || !schema) {
      return;
    }
    const data: ICstCreateDTO = {
      insert_after: activeCst.id,
      cst_type: activeCst.cst_type,
      alias: generateAlias(activeCst.cst_type, schema),
      term_raw: activeCst.term_raw,
      definition_formal: activeCst.definition_formal,
      definition_raw: activeCst.definition_raw,
      convention: activeCst.convention,
      term_forms: activeCst.term_forms
    };
    handleCreateCst(data);
  }

  function renameCst() {
    if (!activeCst || !schema) {
      return;
    }
    const data: ICstRenameDTO = {
      target: activeCst.id,
      alias: activeCst.alias,
      cst_type: activeCst.cst_type
    };
    setRenameInitialData(data);
    showRenameCst({
      schema: schema,
      initial: data,
      allowChangeType: !activeCst.is_inherited,
      onRename: handleRenameCst
    });
  }

  function substitute() {
    if (!schema || (isModified && !promptUnsaved())) {
      return;
    }
    showSubstituteCst({ schema: schema, onSubstitute: handleSubstituteCst });
  }

  function inlineSynthesis() {
    if (!schema || (isModified && !promptUnsaved())) {
      return;
    }
    showInlineSynthesis({ receiver: schema, onInlineSynthesis: handleInlineSynthesis });
  }

  function promptDeleteCst() {
    if (!schema) {
      return;
    }
    showDeleteCst({ schema: schema, selected: selected, onDelete: handleDeleteCst });
  }

  function editTermForms() {
    if (!activeCst) {
      return;
    }
    if (isModified && !promptUnsaved()) {
      return;
    }
    showEditTerm({ target: activeCst, onSave: handleSaveWordforms });
  }

  function reindex() {
    if (!itemID) {
      return;
    }
    resetAliasesInternal(itemID, () => toast.success(information.reindexComplete));
  }

  function reorder() {
    if (!itemID) {
      return;
    }
    restoreOrderInternal(itemID, () => toast.success(information.reorderComplete));
  }

  function produceStructure() {
    if (!activeCst) {
      return;
    }
    if (isModified && !promptUnsaved()) {
      return;
    }
    produceStructureInternal({ itemID: itemID, data: { target: activeCst.id } }, cstList => {
      toast.success(information.addedConstituents(cstList.length));
      if (cstList.length !== 0) {
        setSelected(cstList);
      }
    });
  }

  function handleSetEditors(newEditors: UserID[]) {
    setItemEditors({ itemID: itemID, editors: newEditors }, () => toast.success(information.changesSaved));
  }

  function promptTemplate() {
    if ((isModified && !promptUnsaved()) || !schema) {
      return;
    }
    showCstTemplate({ schema: schema, onCreate: handleCreateCst, insertAfter: activeCst?.id });
  }

  function promptClone() {
    if (!schema || (isModified && !promptUnsaved())) {
      return;
    }
    showClone({
      base: schema,
      initialLocation: calculateCloneLocation(),
      selected: selected,
      totalCount: schema.items.length
    });
  }

  function promptEditors() {
    if (!schema) {
      return;
    }
    showEditEditors({ editors: schema.editors, setEditors: handleSetEditors });
  }

  function promptLocation() {
    if (!schema) {
      return;
    }
    showEditLocation({ initial: schema.location, onChangeLocation: handleSetLocation });
  }

  function download() {
    if ((isModified && !promptUnsaved()) || !schema) {
      return;
    }
    const fileName = (schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
    downloadFile({ itemID: itemID, version: versionID }, (data: Blob) => {
      try {
        fileDownload(data, fileName);
      } catch (error) {
        console.error(error);
      }
    });
  }

  function share() {
    const currentRef = window.location.href;
    const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
    navigator.clipboard
      .writeText(url)
      .then(() => toast.success(information.linkReady))
      .catch(console.error);
  }

  function setOwner(newOwner: UserID) {
    setItemOwner({ itemID: itemID, owner: newOwner }, () => toast.success(information.changesSaved));
  }

  function setAccessPolicy(newPolicy: AccessPolicy) {
    setItemAccessPolicy({ itemID: itemID, policy: newPolicy }, () => toast.success(information.changesSaved));
  }

  function generateQR(): string {
    const currentRef = window.location.href;
    return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
  }

  return (
    <RSEditContext
      value={{
        schema: schema,
        selected,

        isOwned,
        isArchive,
        isMutable,
        isContentEditable,
        isProcessing,
        isAttachedToOSS,
        canProduceStructure,
        canDeleteSelected,

        setOwner,
        setAccessPolicy,
        promptEditors,
        promptLocation,

        setSelected: setSelected,
        select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
        deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
        toggleSelect: (target: ConstituentaID) =>
          setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
        deselectAll: () => setSelected([]),

        viewOSS,
        viewVersion,
        viewPredecessor,
        createVersion,
        restoreVersion,
        promptEditVersions,

        moveUp,
        moveDown,
        createCst,
        cloneCst,
        renameCst,
        promptDeleteCst,
        editTermForms,

        promptTemplate,
        promptClone,
        promptUpload: () => showUpload({ itemID: model.itemID! }),
        download,
        share,

        reindex,
        reorder,
        inlineSynthesis,
        produceStructure,
        substitute,

        showTypeGraph: () => showTypeGraph({ items: typeInfo }),
        showQR: () => showQR({ target: generateQR() })
      }}
    >
      {children}
    </RSEditContext>
  );
};

// ====== Internals =========
function getNextActiveOnDelete(
  activeID: ConstituentaID | undefined,
  items: IConstituenta[],
  deleted: ConstituentaID[]
): ConstituentaID | undefined {
  if (items.length === deleted.length) {
    return undefined;
  }

  let activeIndex = items.findIndex(cst => cst.id === activeID);
  if (activeIndex === -1) {
    return undefined;
  }

  while (activeIndex < items.length && deleted.find(id => id === items[activeIndex].id)) {
    ++activeIndex;
  }
  if (activeIndex >= items.length) {
    activeIndex = items.length - 1;
    while (activeIndex >= 0 && deleted.find(id => id === items[activeIndex].id)) {
      --activeIndex;
    }
  }
  return items[activeIndex].id;
}
