import fs from "fs";
import os from "os";
import path from "path";
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'form... Remove this comment to see the full error message
import File from "formidable/lib/file";
import invariant from "invariant";
import { values, keys } from "lodash";
import { v4 as uuidv4 } from "uuid";
import { parseOutlineExport } from "@shared/utils/zip";
import Logger from "@server/logging/logger";
import { Attachment, Event, Document, Collection, User } from "@server/models";
import { FileImportError } from "../errors";
import attachmentCreator from "./attachmentCreator";
import documentCreator from "./documentCreator";
import documentImporter from "./documentImporter";

export default async function collectionImporter({
  file,
  type,
  user,
  ip,
}: {
  file: File;
  // @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
  user: User;
  type: "outline";
  ip: string;
}) {
  // load the zip structure into memory
  const zipData = await fs.promises.readFile(file.path);
  let items;

  try {
    items = await await parseOutlineExport(zipData);
  } catch (err) {
    throw FileImportError(err.message);
  }

  if (!items.filter((item) => item.type === "document").length) {
    throw FileImportError(
      "Uploaded file does not contain importable documents"
    );
  }

  // store progress and pointers
  // @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message
  const collections: {
    // @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
    string: Collection;
  } = {};
  // @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message
  const documents: {
    string: Document;
  } = {};
  // @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message
  const attachments: {
    // @ts-expect-error ts-migrate(2749) FIXME: 'Attachment' refers to a value, but is being used ... Remove this comment to see the full error message
    string: Attachment;
  } = {};

  for (const item of items) {
    if (item.type === "collection") {
      // check if collection with name exists
      const [collection, isCreated] = await Collection.findOrCreate({
        where: {
          teamId: user.teamId,
          name: item.name,
        },
        defaults: {
          createdById: user.id,
          permission: "read_write",
        },
      });

      // create new collection if name already exists, yes it's possible that
      // there is also a "Name (Imported)" but this is a case not worth dealing
      // with right now
      if (!isCreated) {
        const name = `${item.name} (Imported)`;
        // @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'collection' because it is a cons... Remove this comment to see the full error message
        collection = await Collection.create({
          teamId: user.teamId,
          createdById: user.id,
          name,
          permission: "read_write",
        });
        await Event.create({
          name: "collections.create",
          collectionId: collection.id,
          teamId: collection.teamId,
          actorId: user.id,
          data: {
            name,
          },
          ip,
        });
      }

      collections[item.path] = collection;
      continue;
    }

    if (item.type === "document") {
      const collectionDir = item.dir.split("/")[0];
      const collection = collections[collectionDir];
      invariant(collection, `Collection must exist for document ${item.dir}`);
      // we have a document
      const content = await item.item.async("string");
      const name = path.basename(item.name);
      const tmpDir = os.tmpdir();
      const tmpFilePath = `${tmpDir}/upload-${uuidv4()}`;
      await fs.promises.writeFile(tmpFilePath, content);
      const file = new File({
        name,
        type: "text/markdown",
        path: tmpFilePath,
      });
      const { text, title } = await documentImporter({
        file,
        user,
        ip,
      });
      await fs.promises.unlink(tmpFilePath);
      // must be a nested document, find and reference the parent document
      let parentDocumentId;

      if (item.depth > 1) {
        const parentDocument =
          documents[`${item.dir}.md`] || documents[item.dir];
        invariant(parentDocument, `Document must exist for parent ${item.dir}`);
        parentDocumentId = parentDocument.id;
      }

      const document = await documentCreator({
        source: "import",
        title,
        text,
        publish: true,
        collectionId: collection.id,
        createdAt: item.metadata.createdAt
          ? new Date(item.metadata.createdAt)
          : // @ts-expect-error ts-migrate(2339) FIXME: Property 'date' does not exist on type 'Item'.
            item.date,
        // @ts-expect-error ts-migrate(2339) FIXME: Property 'date' does not exist on type 'Item'.
        updatedAt: item.date,
        parentDocumentId,
        user,
        ip,
      });
      documents[item.path] = document;
      continue;
    }

    if (item.type === "attachment") {
      const buffer = await item.item.async("nodebuffer");
      const attachment = await attachmentCreator({
        source: "import",
        name: item.name,
        type,
        buffer,
        user,
        ip,
      });
      attachments[item.path] = attachment;
      continue;
    }

    Logger.info("commands", `Skipped importing ${item.path}`);
  }

  // All collections, documents, and attachments have been created – time to
  // update the documents to point to newly uploaded attachments where possible
  for (const attachmentPath of keys(attachments)) {
    const attachment = attachments[attachmentPath];

    for (const document of values(documents)) {
      // pull the collection and subdirectory out of the path name, upload folders
      // in an Outline export are relative to the document itself
      const normalizedAttachmentPath = attachmentPath.replace(
        /(.*)uploads\//,
        "uploads/"
      );
      // @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
      document.text = document.text
        .replace(attachmentPath, attachment.redirectUrl)
        .replace(normalizedAttachmentPath, attachment.redirectUrl)
        .replace(`/${normalizedAttachmentPath}`, attachment.redirectUrl);
      // does nothing if the document text is unchanged
      // @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'.
      await document.save({
        fields: ["text"],
      });
    }
  }

  // reload collections to get document mapping
  for (const collection of values(collections)) {
    await collection.reload();
  }

  return {
    documents: values(documents),
    collections: values(collections),
    attachments: values(attachments),
  };
}
