<script setup lang="ts">
import { ref, onMounted, computed, nextTick, onUnmounted } from "vue";
import _ from "lodash";
import { v4 as uuid4 } from "uuid";
import {
  S3Client,
  PutObjectCommand,
  ListObjectsCommand,
  GetObjectCommand,
  DeleteObjectsCommand,
  CopyObjectCommand,
} from "@aws-sdk/client-s3";
import { Plus, Refresh } from "@element-plus/icons-vue";
import Node from "./Node.vue";
import AppendModal from "./AppendModal.vue";
import type { TreeData } from "./Tree";
import { ElNotification, type ElTree } from "element-plus";
import { MdEditor } from "md-editor-v3";
import { s3ContentsToTree, getTreeFlatten } from "@/utils/s3Helper";
import type ETNode from "element-plus/es/components/tree/src/model/node";
import type { DragEvents } from "element-plus/es/components/tree/src/model/useDragNode";
import type {
  AllowDropType,
  NodeDropType,
} from "element-plus/es/components/tree/src/tree.type";

import path from "path-browserify";

const s3 = new S3Client({
  credentials: {
    accessKeyId: import.meta.env.VITE_AWS_S3_ACCESS_KEY_ID,
    secretAccessKey: import.meta.env.VITE_AWS_S3_SECRET_ACCESS_KEY,
  },
  region: import.meta.env.VITE_AWS_S3_REGION,
});

const SIDEBAR_SORTED_MAP_KEY = "SIDEBAR_SORTED_MAP.json";

const tree$ = ref<InstanceType<typeof ElTree>>();

const langSelect = ref<"zh-CN" | "zh-HK" | "en-US">("zh-CN");
const langKeyPrefix = computed(() => {
  return {
    "zh-CN": "docs/",
    "zh-HK": "zh-HK/docs/",
    "en-US": "en-US/docs/",
  }[langSelect.value];
});
const langDataSource = ref<
  Partial<Record<"zh-CN" | "zh-HK" | "en-US", TreeData[]>>
>({});
const langSelectedDataSource = computed(() => {
  return langDataSource.value[langSelect.value];
});
const dataSource = ref<unknown[]>([]);
const sideLoading = ref(false);
const loadS3DocsListObjects = async () => {
  const command = new ListObjectsCommand({
    Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
  });
  const result = await s3.send(command);
  return result.Contents!;
};
const loadS3SideBarSort = async (lang) => {
  try {
    const command = new GetObjectCommand({
      Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
      Key: `${lang}::${SIDEBAR_SORTED_MAP_KEY}`,
      ResponseCacheControl: "no-cache",
    });
    const file = await s3.send(command);
    return JSON.parse(await file.Body?.transformToString()!);
  } catch (e) {
    console.warn(e);
  }
  return {};
};
const loadSideBar = async () => {
  sideLoading.value = true;
  dataSource.value = await loadS3DocsListObjects();
  const allLangs: ("zh-CN" | "zh-HK" | "en-US")[] = ["zh-CN", "zh-HK", "en-US"];
  await Promise.all(
    allLangs.map(async (lang: "zh-CN" | "zh-HK" | "en-US") => {
      const prefix = {
        "zh-CN": "docs/",
        "zh-HK": "zh-HK/docs/",
        "en-US": "en-US/docs/",
      }[lang];
      const sideBarSortMap = await loadS3SideBarSort(lang);
      const filtered = dataSource.value
        .filter((cont) => cont.Key.startsWith(prefix))
        .map((cont) => ({
          ...cont,
          Key: cont.Key.replace(new RegExp(`^${prefix}`), ""),
        }))
        .sort((a, b) => sideBarSortMap[a.Key] - sideBarSortMap[b.Key]);
      // TODO sort
      langDataSource.value[lang] = s3ContentsToTree(
        filtered,
        {},
        (r, label, i, a, thisContent) => ({
          isDir: a.length !== i + 1,
          ...(a.length === i + 1 ? thisContent : {}),
        })
      );
    })
  );

  sideLoading.value = false;
};
onMounted(() => {
  loadSideBar();
});

const currentOpenData = ref<TreeData>();

const onNodeClick = async (data: TreeData, node) => {
  if (data.isDir) {
    return;
  }
  textLoading.value = true;
  const command = new GetObjectCommand({
    Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
    Key: `${langKeyPrefix.value}${data.key}`,
    ResponseCacheControl: "no-cache",
  });
  const file = await s3.send(command);
  text.value = await file.Body?.transformToString()!;
  currentOpenData.value = data;

  textLoading.value = false;
};

const text = ref("");
const textLoading = ref(false);

const onUploadImg = async (
  files: File[],
  callback: (
    urls: string[] | { url: string; alt: string; title: string }[]
  ) => void
) => {
  let result = [];
  for (const file of files) {
    try {
      const key = `${uuid4()}::${file.name}`;
      const command = new PutObjectCommand({
        Bucket: import.meta.env.VITE_DOCS_MEDIA_BUCKET,
        Key: key,
        Body: file,
        ACL: "public-read",
      });
      const res = await s3.send(command);
      result.push({
        url: `https://${
          import.meta.env.VITE_DOCS_MEDIA_BUCKET
        }.s3.amazonaws.com/${key}`,
        alt: file.name,
        title: file.name,
      });
    } catch (e) {
      console.error(e);
      ElNotification.error(`${file.name} 上传失败`);
    }
  }
  callback(result);
};
const onSave = async () => {
  if (!currentOpenData.value) {
    ElNotification.info("请先选择文件");
    return;
  }
  textLoading.value = true;
  try {
    const command = new PutObjectCommand({
      Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
      Key: `${langKeyPrefix.value}${currentOpenData.value?.key}`,
      Body: text.value,
    });
    await s3.send(command);
    ElNotification.success(`${currentOpenData.value?.key} 保存成功`);
  } catch (e) {
    console.error(e);
    ElNotification.error(`${currentOpenData.value?.key} 保存失败`);
  } finally {
    textLoading.value = false;
  }
};

let resolveAppend: Function | undefined;
let rejectAppend: Function | undefined;
const showAppendModal = ref(false);
const appendLoading = ref(false);
const appendContextData = ref();
const onAppend = async (data: TreeData) => {
  const modalConfirmPromise = new Promise((resolve, reject) => {
    resolveAppend = resolve;
    rejectAppend = reject;
  });
  appendContextData.value = data;
  showAppendModal.value = true;
  let filename, isDir;
  try {
    ({ filename, isDir } = await modalConfirmPromise);
  } catch (e) {
    return;
  }
  try {
    appendLoading.value = true;
    // const key = `${langKeyPrefix.value}${data.key ? `${data.key}/` : ''}${filename}`
    const key = path.join(langKeyPrefix.value, data.key ?? "", filename);
    if (!isDir) {
      const command = new PutObjectCommand({
        Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
        Key: key,
        Body: "",
      });
      await s3.send(command);
    }
    const newChild: TreeData = {
      key: `${data.key ? `${data.key}/` : ""}${filename}`,
      label: filename,
      children: [],
      isDir,
    };
    if (!data.children) {
      data.children = [];
    }
    data.children.push(newChild);
  } catch (e) {
    console.error(e);
    ElNotification.error("添加失败");
  } finally {
    appendLoading.value = false;
    showAppendModal.value = false;
  }
  if (!isDir) {
    saveSideBarSort();
  }
};
const onRemove = async (node: ETNode, data: TreeData) => {
  const parent = node.parent;
  const children = parent.data.children || parent.data;
  const index = children.findIndex((d) => d.key === data.key);

  sideLoading.value = true;
  try {
    const leafDatas = getTreeFlatten(data);
    const command = new DeleteObjectsCommand({
      Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
      Delete: {
        Objects: leafDatas.map((d) => ({
          Key: `${langKeyPrefix.value}${d.key}`,
        })),
        Quiet: false,
      },
    });
    await s3.send(command);
    children.splice(index, 1);
  } catch (e) {
    console.error(e);
    ElNotification.error("删除失败");
  } finally {
    sideLoading.value = false;
  }
};
const saveSideBarSort = async () => {
  sideLoading.value = true;
  const sortedNodes = getTreeFlatten(
    { children: tree$.value!.data } as TreeData,
    "preorder",
    "all"
  );
  const sortedKeyMap = sortedNodes.reduce(
    (a, v, i) => ({ ...a, [path.join(...v.__path__!)]: i }),
    {}
  );
  const command = new PutObjectCommand({
    Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
    Key: `${langSelect.value}::${SIDEBAR_SORTED_MAP_KEY}`,
    Body: JSON.stringify(sortedKeyMap),
  });
  await s3.send(command);
  sideLoading.value = false;
};
const onDragEnd = async (
  draggingNode: ETNode,
  dropNode: ETNode,
  dropType: NodeDropType,
  ev: DragEvents
) => {
  if (dropType === "none") {
    return;
  }
  sideLoading.value = true;

  const copySourceNodes = getTreeFlatten(draggingNode.data);
  const waitForDelete = [];
  for (const sourceNode of copySourceNodes) {
    if (sourceNode.isDir) {
      continue;
    }

    const copySource = path.join(langKeyPrefix.value, sourceNode.key);
    const copyTarget = path.join(
      langKeyPrefix.value,
      dropType === "inner" ? dropNode.data.key : dropNode.parent.data.key ?? "",
      path.basename(draggingNode.data.key),
      path.relative(draggingNode.data.key, sourceNode.key)
    );
    if (copySource === copyTarget) {
      continue;
    }
    try {
      const command = new CopyObjectCommand({
        Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
        CopySource: encodeURIComponent(
          `/${import.meta.env.VITE_DOCS_LIST_BUCKET}/${copySource}`
        ),
        Key: copyTarget,
      });
      await s3.send(command);
      waitForDelete.push({ Key: copySource });
    } catch (e) {
      console.error(e);
      console.warn(`${copySource} 复制失败`);
    }
  }
  if (waitForDelete.length) {
    const command = new DeleteObjectsCommand({
      Bucket: import.meta.env.VITE_DOCS_LIST_BUCKET,
      Delete: {
        Objects: waitForDelete,
        Quiet: false,
      },
    });
    await s3.send(command);
  }

  sideLoading.value = false;
  nextTick(async () => {
    await saveSideBarSort();
    await loadSideBar();
  });
};
const allowDrop = (draggingNode: Node, dropNode: Node, type: AllowDropType) => {
  return !(!dropNode.data.isDir && type === "inner");
};

let observer: InstanceType<typeof MutationObserver>
const callback = (mutationList: MutationRecord[]) => {
  const preview$ = ( mutationList.at(-1)?.target as HTMLElement ).querySelector('#helpdocs-editor-preview')
  preview$?.classList.remove('md-editor-preview')
};
onMounted(() => {
  const el = document.querySelector('#helpdocs-editor')
  if (el) {
    observer = new MutationObserver(callback)
    observer.observe(el, { childList: true, subtree: true })
  }
})
onUnmounted(() => {
  observer && observer.disconnect()
})
</script>
<template>
  <div class="md-container">
    <div class="left" v-loading="sideLoading">
      <div class="toolbar">
        <el-segmented
          v-model="langSelect"
          :options="[
            { label: '简体中文', value: 'zh-CN' },
            { label: '繁体中文', value: 'zh-HK' },
            { label: 'English', value: 'en-US' },
          ]"
        />
        <el-button-group>
          <el-button
            @click="onAppend({ key: '', label: '', children: langSelectedDataSource })"
            type="primary"
            text
            :icon="Plus"
          >
          </el-button>
          <el-button @click="loadSideBar" type="info" text :icon="Refresh"></el-button>
        </el-button-group>
      </div>
      <el-divider />
      <el-tree
        ref="tree$"
        :data="langSelectedDataSource"
        node-key="key"
        default-expand-all
        :expand-on-click-node="false"
        @node-click="onNodeClick"
        draggable
        :allow-drop="allowDrop"
        @node-drag-end="onDragEnd"
      >
        <template #default="{ node, data }">
          <Node
            :node="node"
            :data="data"
            @append="() => onAppend(data)"
            @remove="() => onRemove(node, data)"
          />
        </template>
      </el-tree>
    </div>
    <MdEditor
      editorId="helpdocs-editor"
      v-model="text"
      v-loading="textLoading"
      :disabled="!currentOpenData"
      :toolbarsExclude="['github']"
      previewTheme="custom"
      class="vp-doc"
      @save="onSave"
      @uploadImg="onUploadImg"
    />
  </div>
  <AppendModal
    v-model:show="showAppendModal"
    :contextData="appendContextData"
    :appendLoading="appendLoading"
    @ok="(formData) => resolveAppend?.(formData)"
    @cancel="() => rejectAppend?.()"
  />
</template>

<style src="md-editor-v3/lib/style.css"></style>
<style lang="scss" scoped>
.md-container {
  width: 100vw;
  height: 100dvh;
  display: flex;
  align-items: stretch;
  .md-editor {
    flex: 1;
    height: 100%;
  }
  .left {
    flex: 0 0 300px;
    padding: 10px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    .toolbar {
      display: flex;
      flex-direction: column;
      gap: 5px;
    }
    .el-tree {
      overflow: auto;
      flex: 1;
      :deep(.el-tree-node.is-current) {
        & > .el-tree-node__content {
          background-color: aqua;
        }
      }
    }
  }
}
</style>
