import { BlockEntity } from "@logseq/libs/dist/LSPlugin.user";
import { parseEDNString, toEDNString } from "edn-data";
import { decode, encode } from "js-base64";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { filter } from "rxjs";

import { change$ } from "./observables";
import { Mode, ProgressBar } from "./progress-bar";
import style from "./style.tcss?raw";

const macroPrefix = ":todomaster";

const allMarkers = [
  "done",
  "now",
  "later",
  "doing", // maps to now
  "todo", // maps to later
] as const;

type Marker = typeof allMarkers[number];

const reduceToMap = (vals?: Marker[]) => {
  function unify(m: Marker): Exclude<Marker, "doing" | "todo"> {
    if (m === "todo") {
      return "later";
    }
    if (m === "doing") {
      return "now";
    }
    return m;
  }
  return (vals ?? []).reduce(
    (acc, val) => {
      const k = unify(val);
      acc[k] = acc[k] + 1;
      return acc;
    },
    {
      done: 0,
      later: 0,
      now: 0,
    }
  );
};

async function getTODOStats(maybeUUID: string) {
  const result = await getBlockMarkers(maybeUUID);
  return result
    ? { mapping: reduceToMap(result.markers), mode: result.mode }
    : null;
}

function checkIsUUid(maybeUUID: string) {
  return maybeUUID.length === 36 && maybeUUID.includes("-");
}

// Get the body from the following ...
// #+BEGIN_QUERY
// {:title "{{renderer :todomaster}}"
//  :query [:find (pull ?b [*])
//                  :where
//                 [?b :block/marker _]]}
// #+END_QUERY
function getQueryFromContent(_content: string) {
  try {
    let content = _content.trim();
    const startToken = "#+BEGIN_QUERY";
    const endToken = "#+END_QUERY";

    const startIndex = content.indexOf(startToken);
    const endIndex = content.indexOf(endToken);

    if (startIndex === -1 || endIndex === -1) {
      return null;
    }

    content = content.substring(startIndex + startToken.length, endIndex);
    const contentEDN = (parseEDNString(content) as any).map;
    const query = toEDNString(
      contentEDN.find((r: any) => r[0].key === "query")?.[1]
    );
    // TODO: Logseq inputs can contain magic strings, like :today etc
    // TODO: Need to transform them before passing to DataScript.
    // ref: https://github.com/logseq/logseq/blob/130728adcd7acd4250a78a4e34b1c2d69c0ca3e1/src/main/frontend/db/query_react.cljs#L17-L49
    const inputs: string[] = contentEDN
      .find((r: any) => r[0].key === "inputs")?.[1]
      ?.map(toEDNString);
    return { query, inputs };
  } catch (err) {
    // ignore error
  }
  return null;
}

// Get a simple query from the content
function getSimpleQueryFromContent(_content: string) {
  return _content.trim().match(/{{query\s+(.*)\s*}}/)?.[1];
}

// By default, if UUID is valid, get the block children nodes
// If UUID is not valid and it is rendered in a query block, get the query and render the result
// If the current node is
async function getBlockTreeAndMode(maybeUUID: string) {
  let tree: Partial<BlockEntity> | null = null;
  let mode: Mode | null = null;

  if (checkIsUUid(maybeUUID)) {
    tree = await logseq.Editor.getBlock(maybeUUID, { includeChildren: true });
  }

  if (tree?.content) {
    const queryAndInputs = getQueryFromContent(tree?.content);
    const simpleQuery = getSimpleQueryFromContent(tree?.content);
    if (queryAndInputs) {
      const result = (
        await logseq.DB.datascriptQuery(
          queryAndInputs.query,
          ...(queryAndInputs.inputs ?? [])
        )
      )?.flat();
      mode = "query";
      tree = { children: result };
    } else if (simpleQuery) {
      const result = (await logseq.DB.q(simpleQuery))?.flat();
      mode = "q";
      tree = { children: result };
    }
  }

  if (
    !mode &&
    // If this is the root node and have no children
    tree &&
    tree.children &&
    tree.children.length === 0 &&
    tree.parent?.id &&
    tree.parent?.id === tree.page?.id
  ) {
    const maybePageName = tree?.page?.originalName ?? maybeUUID;

    mode = "page";
    tree = { children: await logseq.Editor.getPageBlocksTree(maybePageName) };
  }

  if (!tree || !tree.children) {
    return null; // Block/page not found
  }

  mode = mode || "block";

  return { tree, mode };
}

async function getBlockMarkers(
  maybeUUID: string
): Promise<{ markers: Marker[]; mode: Mode } | null> {
  const res: any[] = [];
  function traverse(tree: any) {
    if (tree.children) {
      for (const child of tree.children) {
        traverse(child);
      }
    }

    if (tree.uuid && tree.marker && tree.uuid !== maybeUUID) {
      res.push(tree.marker.toLowerCase());
    }
  }

  const maybeTreeAndMode = await getBlockTreeAndMode(maybeUUID);

  if (maybeTreeAndMode) {
    traverse(maybeTreeAndMode.tree);
    return {
      markers: res,
      mode: maybeTreeAndMode.mode,
    };
  }

  return null;
}
const pluginId = "todomaster";

const delay = (t: number) =>
  new Promise((res) => {
    setTimeout(res, t);
  });

function slotExists(slot: string) {
  return Promise.race([
    Promise.resolve(logseq.App.queryElementById(slot)),
    delay(1000).then(() => false),
  ]);
}

// slot -> rendering state
const rendering = new Map<string, { maybeUUID: string; template: string }>();

async function render(maybeUUID: string, slot: string, counter: number) {
  try {
    if (rendering.get(slot)?.maybeUUID !== maybeUUID) {
      return;
    }
    const status = await getTODOStats(maybeUUID);
    if (rendering.get(slot)?.maybeUUID !== maybeUUID) {
      return;
    }
    const template = ReactDOMServer.renderToStaticMarkup(
      <ProgressBar status={status?.mapping} mode={status?.mode} />
    );

    // See https://github.com/logseq/logseq-plugin-samples/blob/master/logseq-pomodoro-timer/index.ts#L92
    if (counter === 0 || (await slotExists(slot))) {
      // No need to rerender if template is the same
      if (rendering.get(slot)?.template === template) {
        return true;
      }
      rendering.get(slot)!.template = template;
      logseq.provideUI({
        key: pluginId + "__" + slot,
        slot,
        reset: true,
        template: template,
      });
      return true;
    }
  } catch (err: any) {
    console.error(err);
    // skip invalid
  }
}

async function startRendering(maybeUUID: string, slot: string) {
  rendering.set(slot, { maybeUUID, template: "" });
  let counter = 0;

  const unsub = change$.subscribe(async (e) => {
    await render(maybeUUID, slot, counter++);
    const exist = await slotExists(slot);
    if (!exist) {
      rendering.delete(slot);
      if (!unsub.closed) {
        unsub.unsubscribe();
      }
    }
  });
}

export function registerCommand() {
  logseq.provideStyle(style);

  logseq.App.onMacroRendererSlotted(async ({ payload, slot }) => {
    const [type] = payload.arguments;
    if (!type?.startsWith(macroPrefix)) {
      return;
    }

    logseq.provideStyle({
      key: slot,
      style: `#${slot} {display: inline-flex;}`,
    });

    let maybeUUID = null;
    // Implicitly use the current block
    if (type === macroPrefix) {
      maybeUUID = payload.uuid;
    } else {
      maybeUUID = decode(type.substring(macroPrefix.length + 1));
    }
    if (maybeUUID) {
      startRendering(maybeUUID, slot);
    }
  });

  async function insertMacro(mode: "page" | "block" = "block") {
    const block = await logseq.Editor.getCurrentBlock();
    if (block && block.uuid) {
      let content = "";
      let maybeUUID = "";
      if (mode === "block") {
        // We will from now on always use implicit block IDs to get rid of "Tracking target not found" issue
        // maybeUUID = block.uuid;
      } else {
        const page = await logseq.Editor.getPage(block.page.id);
        if (page?.originalName) {
          maybeUUID = page.originalName;
        }
      }
      if (maybeUUID) {
        // Use base64 to avoid incorrectly rendering in properties
        content = `{{renderer ${macroPrefix}-${encode(maybeUUID)}}}`;
      } else {
        content = `{{renderer ${macroPrefix}}}`;
      }
      await logseq.Editor.insertAtEditingCursor(content);
    }
  }

  logseq.Editor.registerSlashCommand(
    "[TODO Master] Add Progress Bar",
    async () => {
      return insertMacro();
    }
  );
}