import { useBlockProps } from "@wordpress/block-editor";
import {
  GraphEmbedderMessageCallbacks,
  Subgraph,
  EntityRootType,
  VersionedUrl,
  RemoteFileEntity,
} from "@blockprotocol/graph";
import { buildSubgraph } from "@blockprotocol/graph/stdlib";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import {
  blockSubgraphResolveDepths,
  createEntity as apiCreateEntity,
  deleteEntity as apiDeleteEntity,
  updateEntity as apiUpdateEntity,
  uploadFile as apiUploadFile,
  dbEntityToEntity,
  getEntitySubgraph,
  queryEntities,
} from "./shared";

import { BlockLoader } from "./block-loader";
import { CustomBlockControls } from "./edit/block-controls";
import { LoadingImage } from "./edit/loading-image";
import { ServiceEmbedderMessageCallbacks } from "@blockprotocol/service/.";
import { constructServiceModuleCallbacks } from "./edit/service-callbacks";

type BlockProtocolBlockAttributes = {
  author: string;
  blockName: string;
  entityId: string;
  entityTypeId: string;
  preview: boolean;
  protocol: string;
  sourceUrl: string;
};

type EditProps = {
  attributes: BlockProtocolBlockAttributes;
  setAttributes: (attributes: Partial<BlockProtocolBlockAttributes>) => void;
};

/**
 * The admin view of the block – the block is in editable mode, with callbacks to create, update, and delete entities
 */
export const Edit = ({
  attributes: { blockName, entityId, entityTypeId, preview, sourceUrl },
  setAttributes,
}: EditProps) => {
  const blockProps = useBlockProps();

  const [entitySubgraph, setEntitySubgraph] =
    useState<Subgraph<EntityRootType> | null>(null);

  // this represents the latest versions of blocks from the Block Protocol API
  // the page may contain older versions of blocks, so do not rely on all blocks being here
  const blocks = window.block_protocol_data?.blocks;

  const selectedBlock = blocks?.find((block) => block.source === sourceUrl);

  if (preview) {
    // if we're previewing blocks we're coming from the block selector – should only be loading latest
    if (!selectedBlock) {
      throw new Error("No block data from server – could not preview");
    }

    return (
      <img
        src={
          selectedBlock?.image
            ? selectedBlock.image
            : "https://blockprotocol.org/assets/default-block-img.svg"
        }
        style={{
          width: "100%",
          height: "auto",
          objectFit: "contain",
        }}
      />
    );
  }

  const setEntityId = (entityId: string) => setAttributes({ entityId });

  const creating = useRef(false);

  useEffect(() => {
    if (creating.current) {
      return;
    }

    if (!entityId) {
      creating.current = true;
      apiCreateEntity({
        entityTypeId,
        properties: {},
        blockMetadata: {
          sourceUrl,
          version: selectedBlock?.version ?? "unknown",
        },
      }).then(({ entity }) => {
        const { entity_id } = entity;
        const subgraph = buildSubgraph(
          {
            entities: [dbEntityToEntity(entity)],
            dataTypes: [],
            entityTypes: [],
            propertyTypes: [],
          },
          [
            {
              entityId: entity.entity_id,
              editionId: new Date(entity.updated_at).toISOString(),
            },
          ],
          blockSubgraphResolveDepths
        );
        setEntitySubgraph(subgraph);
        setEntityId(entity_id);
        creating.current = false;
      });
    } else if (
      !entitySubgraph ||
      entitySubgraph.roots[0]?.baseId !== entityId
    ) {
      getEntitySubgraph({
        data: {
          entityId,
          graphResolveDepths: blockSubgraphResolveDepths,
        },
      }).then(({ data }) => {
        if (!data) {
          throw new Error("No data returned from getEntitySubgraph");
        }
        setEntitySubgraph(data);
      });
    }
  }, [entitySubgraph, entityId]);

  const refetchSubgraph = useCallback(async () => {
    if (!entityId) {
      return;
    }

    const { data: subgraph } = await getEntitySubgraph({
      data: { entityId, graphResolveDepths: blockSubgraphResolveDepths },
    });

    if (!subgraph) {
      throw new Error("No data returned from getEntitySubgraph");
    }

    setEntitySubgraph(subgraph);
  }, [entityId]);

  const serviceCallbacks = useMemo<ServiceEmbedderMessageCallbacks>(
    () => constructServiceModuleCallbacks(),
    []
  );

  const graphCallbacks = useMemo<
    Required<
      Pick<
        GraphEmbedderMessageCallbacks,
        | "createEntity"
        | "deleteEntity"
        | "getEntity"
        | "updateEntity"
        | "uploadFile"
        | "queryEntities"
      >
    >
  >(
    () => ({
      getEntity: getEntitySubgraph,
      createEntity: async ({ data }) => {
        if (!data) {
          return {
            errors: [
              {
                message: "No data provided in createEntity request",
                code: "INVALID_INPUT",
              },
            ],
          };
        }

        const creationData = data;

        const { entity: createdEntity } = await apiCreateEntity(creationData);

        refetchSubgraph(); // @todo should we await this – slows down response but ensures no delay between entity update + subgraph update

        return { data: dbEntityToEntity(createdEntity) };
      },
      updateEntity: async ({ data }) => {
        if (!data) {
          return {
            errors: [
              {
                message: "No data provided in updateEntity request",
                code: "INVALID_INPUT",
              },
            ],
          };
        }

        const { entityId, properties, leftToRightOrder, rightToLeftOrder } =
          data;

        try {
          const { entity: updatedDbEntity } = await apiUpdateEntity(entityId, {
            properties,
            leftToRightOrder,
            rightToLeftOrder,
          });

          refetchSubgraph(); // @todo should we await this – slows down response but ensures no delay between entity update + subgraph update

          return {
            data: dbEntityToEntity(updatedDbEntity),
          };
        } catch (err) {
          return {
            errors: [
              {
                message: `Error when processing update of entity ${entityId}: ${err}`,
                // @todo make INTERNAL_ERROR or UNKNOWN_ERROR permitted
                code: "INVALID_INPUT",
              },
            ],
          };
        }
      },
      deleteEntity: async ({ data }) => {
        if (!data) {
          return {
            errors: [
              {
                message: "No data provided in deleteEntity request",
                code: "INVALID_INPUT",
              },
            ],
          };
        }

        const { entityId } = data;

        try {
          await apiDeleteEntity(entityId); // @todo error handling
        } catch (err) {
          return {
            errors: [
              {
                message: `Error when processing deletion of entity ${entityId}: ${err}`,
                // @todo make INTERNAL_ERROR or UNKNOWN_ERROR permitted
                code: "INVALID_INPUT",
              },
            ],
          };
        }

        refetchSubgraph(); // @todo should we await this – slows down response but ensures no delay between entity update + subgraph update

        return { data: true };
      },
      uploadFile: async ({ data }) => {
        if (!data) {
          throw new Error("No data provided in uploadFile request");
        }

        try {
          const { entity } = await apiUploadFile(data);
          return {
            data: dbEntityToEntity(entity) as RemoteFileEntity,
          };
        } catch (err) {
          return {
            errors: [
              {
                message: `Error when processing file upload: ${err}`,
                // @todo make INTERNAL_ERROR or UNKNOWN_ERROR permitted
                code: "INVALID_INPUT",
              },
            ],
          };
        }
      },

      queryEntities: async ({ data }) => {
        if (!data) {
          throw new Error("No data provided in queryEntities request");
        }

        try {
          const { entities } = await queryEntities(data);

          const subgraph = buildSubgraph(
            {
              entities: entities.map(dbEntityToEntity),
              dataTypes: [],
              entityTypes: [],
              propertyTypes: [],
            },
            entities.map((entity) => ({
              entityId: entity.entity_id,
              editionId: new Date(entity.updated_at).toISOString(),
            })),
            blockSubgraphResolveDepths
          );

          return {
            data: {
              results: subgraph,
              operation: data.operation,
            },
          };
        } catch (err) {
          return {
            errors: [
              {
                message: `Error when querying entities: ${err}`,
                // @todo make INTERNAL_ERROR or UNKNOWN_ERROR permitted
                code: "INVALID_INPUT",
              },
            ],
          };
        }
      },
    }),
    [refetchSubgraph]
  );

  if (!entitySubgraph) {
    return (
      <div style={{ marginTop: 10 }}>
        <LoadingImage />
      </div>
    );
  }

  return (
    <div {...blockProps} style={{ marginBottom: 30 }}>
      <CustomBlockControls
        entityId={entityId}
        entityTypeId={entityTypeId as VersionedUrl} // @todo fix this in @blockprotocol/graph
        setEntityId={setEntityId}
        entitySubgraph={entitySubgraph}
        updateEntity={graphCallbacks.updateEntity}
      />
      <BlockLoader
        blockName={blockName}
        callbacks={{ graph: graphCallbacks, service: serviceCallbacks }}
        entitySubgraph={entitySubgraph}
        LoadingImage={LoadingImage}
        readonly={false}
        sourceUrl={sourceUrl}
      />
    </div>
  );
};
