'use strict';

const hookable = require('hookable');
const dom = require('@unhead/dom');
const shared = require('@unhead/shared');

const UsesMergeStrategy = ["templateParams", "htmlAttrs", "bodyAttrs"];
const DedupePlugin = shared.defineHeadPlugin({
  hooks: {
    "tag:normalise": function({ tag }) {
      ["hid", "vmid", "key"].forEach((key) => {
        if (tag.props[key]) {
          tag.key = tag.props[key];
          delete tag.props[key];
        }
      });
      const generatedKey = shared.tagDedupeKey(tag);
      const dedupe = generatedKey || (tag.key ? `${tag.tag}:${tag.key}` : false);
      if (dedupe)
        tag._d = dedupe;
    },
    "tags:resolve": function(ctx) {
      const deduping = {};
      ctx.tags.forEach((tag) => {
        const dedupeKey = (tag.key ? `${tag.tag}:${tag.key}` : tag._d) || tag._p;
        const dupedTag = deduping[dedupeKey];
        if (dupedTag) {
          let strategy = tag?.tagDuplicateStrategy;
          if (!strategy && UsesMergeStrategy.includes(tag.tag))
            strategy = "merge";
          if (strategy === "merge") {
            const oldProps = dupedTag.props;
            ["class", "style"].forEach((key) => {
              if (oldProps[key]) {
                if (tag.props[key]) {
                  if (key === "style" && !oldProps[key].endsWith(";"))
                    oldProps[key] += ";";
                  tag.props[key] = `${oldProps[key]} ${tag.props[key]}`;
                } else {
                  tag.props[key] = oldProps[key];
                }
              }
            });
            deduping[dedupeKey].props = {
              ...oldProps,
              ...tag.props
            };
            return;
          } else if (tag._e === dupedTag._e) {
            dupedTag._duped = dupedTag._duped || [];
            tag._d = `${dupedTag._d}:${dupedTag._duped.length + 1}`;
            dupedTag._duped.push(tag);
            return;
          } else if (shared.tagWeight(tag) > shared.tagWeight(dupedTag)) {
            return;
          }
        }
        const propCount = Object.keys(tag.props).length + (tag.innerHTML ? 1 : 0) + (tag.textContent ? 1 : 0);
        if (shared.HasElementTags.includes(tag.tag) && propCount === 0) {
          delete deduping[dedupeKey];
          return;
        }
        deduping[dedupeKey] = tag;
      });
      const newTags = [];
      Object.values(deduping).forEach((tag) => {
        const dupes = tag._duped;
        delete tag._duped;
        newTags.push(tag);
        if (dupes)
          newTags.push(...dupes);
      });
      ctx.tags = newTags;
      ctx.tags = ctx.tags.filter((t) => !(t.tag === "meta" && (t.props.name || t.props.property) && !t.props.content));
    }
  }
});

const PayloadPlugin = shared.defineHeadPlugin({
  mode: "server",
  hooks: {
    "tags:resolve": function(ctx) {
      const payload = {};
      ctx.tags.filter((tag) => ["titleTemplate", "templateParams", "title"].includes(tag.tag) && tag._m === "server").forEach((tag) => {
        payload[tag.tag] = tag.tag.startsWith("title") ? tag.textContent : tag.props;
      });
      Object.keys(payload).length && ctx.tags.push({
        tag: "script",
        innerHTML: JSON.stringify(payload),
        props: { id: "unhead:payload", type: "application/json" }
      });
    }
  }
});

const ValidEventTags = ["script", "link", "bodyAttrs"];
const EventHandlersPlugin = shared.defineHeadPlugin((head) => ({
  hooks: {
    "tags:resolve": function(ctx) {
      for (const tag of ctx.tags.filter((t) => ValidEventTags.includes(t.tag))) {
        Object.entries(tag.props).forEach(([key, value]) => {
          if (key.startsWith("on") && typeof value === "function") {
            if (head.ssr && shared.NetworkEvents.includes(key))
              tag.props[key] = `this.dataset.${key}fired = true`;
            else
              delete tag.props[key];
            tag._eventHandlers = tag._eventHandlers || {};
            tag._eventHandlers[key] = value;
          }
        });
        if (head.ssr && tag._eventHandlers && (tag.props.src || tag.props.href))
          tag.key = tag.key || shared.hashCode(tag.props.src || tag.props.href);
      }
    },
    "dom:renderTag": function({ $el, tag }) {
      for (const k of Object.keys($el?.dataset || {}).filter((k2) => shared.NetworkEvents.some((e) => `${e}fired` === k2))) {
        const ek = k.replace("fired", "");
        tag._eventHandlers?.[ek]?.call($el, new Event(ek.replace("on", "")));
      }
    }
  }
}));

const DupeableTags = ["link", "style", "script", "noscript"];
const HashKeyedPlugin = shared.defineHeadPlugin({
  hooks: {
    "tag:normalise": ({ tag }) => {
      if (tag.key && DupeableTags.includes(tag.tag)) {
        tag.props["data-hid"] = tag._h = shared.hashCode(tag.key);
      }
    }
  }
});

const SortPlugin = shared.defineHeadPlugin({
  hooks: {
    "tags:resolve": (ctx) => {
      const tagPositionForKey = (key) => ctx.tags.find((tag) => tag._d === key)?._p;
      for (const { prefix, offset } of shared.SortModifiers) {
        for (const tag of ctx.tags.filter((tag2) => typeof tag2.tagPriority === "string" && tag2.tagPriority.startsWith(prefix))) {
          const position = tagPositionForKey(
            tag.tagPriority.replace(prefix, "")
          );
          if (typeof position !== "undefined")
            tag._p = position + offset;
        }
      }
      ctx.tags.sort((a, b) => a._p - b._p).sort((a, b) => shared.tagWeight(a) - shared.tagWeight(b));
    }
  }
});

const SupportedAttrs = {
  meta: "content",
  link: "href",
  htmlAttrs: "lang"
};
const TemplateParamsPlugin = shared.defineHeadPlugin((head) => ({
  hooks: {
    "tags:resolve": (ctx) => {
      const { tags } = ctx;
      const title = tags.find((tag) => tag.tag === "title")?.textContent;
      const idx = tags.findIndex((tag) => tag.tag === "templateParams");
      const params = idx !== -1 ? tags[idx].props : {};
      const sep = params.separator || "|";
      delete params.separator;
      params.pageTitle = shared.processTemplateParams(params.pageTitle || title || "", params, sep);
      for (const tag of tags.filter((t) => t.processTemplateParams !== false)) {
        const v = SupportedAttrs[tag.tag];
        if (v && typeof tag.props[v] === "string") {
          tag.props[v] = shared.processTemplateParams(tag.props[v], params, sep);
        } else if (tag.processTemplateParams === true || ["titleTemplate", "title"].includes(tag.tag)) {
          ["innerHTML", "textContent"].forEach((p) => {
            if (typeof tag[p] === "string")
              tag[p] = shared.processTemplateParams(tag[p], params, sep);
          });
        }
      }
      head._templateParams = params;
      head._separator = sep;
      ctx.tags = tags.filter((tag) => tag.tag !== "templateParams");
    }
  }
}));

const TitleTemplatePlugin = shared.defineHeadPlugin({
  hooks: {
    "tags:resolve": (ctx) => {
      const { tags } = ctx;
      let titleTemplateIdx = tags.findIndex((i) => i.tag === "titleTemplate");
      const titleIdx = tags.findIndex((i) => i.tag === "title");
      if (titleIdx !== -1 && titleTemplateIdx !== -1) {
        const newTitle = shared.resolveTitleTemplate(
          tags[titleTemplateIdx].textContent,
          tags[titleIdx].textContent
        );
        if (newTitle !== null) {
          tags[titleIdx].textContent = newTitle || tags[titleIdx].textContent;
        } else {
          delete tags[titleIdx];
        }
      } else if (titleTemplateIdx !== -1) {
        const newTitle = shared.resolveTitleTemplate(
          tags[titleTemplateIdx].textContent
        );
        if (newTitle !== null) {
          tags[titleTemplateIdx].textContent = newTitle;
          tags[titleTemplateIdx].tag = "title";
          titleTemplateIdx = -1;
        }
      }
      if (titleTemplateIdx !== -1) {
        delete tags[titleTemplateIdx];
      }
      ctx.tags = tags.filter(Boolean);
    }
  }
});

const XSSPlugin = shared.defineHeadPlugin({
  hooks: {
    "tags:afterResolve": function(ctx) {
      for (const tag of ctx.tags) {
        if (typeof tag.innerHTML === "string") {
          if (tag.innerHTML && ["application/ld+json", "application/json"].includes(tag.props.type)) {
            tag.innerHTML = tag.innerHTML.replace(/</g, "\\u003C");
          } else {
            tag.innerHTML = tag.innerHTML.replace(new RegExp(`</${tag.tag}`, "g"), `<\\/${tag.tag}`);
          }
        }
      }
    }
  }
});

let activeHead;
// @__NO_SIDE_EFFECTS__
function createHead(options = {}) {
  const head = createHeadCore(options);
  head.use(dom.DomPlugin());
  return activeHead = head;
}
// @__NO_SIDE_EFFECTS__
function createServerHead(options = {}) {
  return activeHead = createHeadCore(options);
}
function filterMode(mode, ssr) {
  return !mode || mode === "server" && ssr || mode === "client" && !ssr;
}
function createHeadCore(options = {}) {
  const hooks = hookable.createHooks();
  hooks.addHooks(options.hooks || {});
  options.document = options.document || (shared.IsBrowser ? document : void 0);
  const ssr = !options.document;
  const updated = () => {
    head.dirty = true;
    hooks.callHook("entries:updated", head);
  };
  let entryCount = 0;
  let entries = [];
  const plugins = [];
  const head = {
    plugins,
    dirty: false,
    resolvedOptions: options,
    hooks,
    headEntries() {
      return entries;
    },
    use(p) {
      const plugin = typeof p === "function" ? p(head) : p;
      if (!plugin.key || !plugins.some((p2) => p2.key === plugin.key)) {
        plugins.push(plugin);
        filterMode(plugin.mode, ssr) && hooks.addHooks(plugin.hooks || {});
      }
    },
    push(input, entryOptions) {
      delete entryOptions?.head;
      const entry = {
        _i: entryCount++,
        input,
        ...entryOptions
      };
      if (filterMode(entry.mode, ssr)) {
        entries.push(entry);
        updated();
      }
      return {
        dispose() {
          entries = entries.filter((e) => e._i !== entry._i);
          hooks.callHook("entries:updated", head);
          updated();
        },
        // a patch is the same as creating a new entry, just a nice DX
        patch(input2) {
          entries = entries.map((e) => {
            if (e._i === entry._i) {
              e.input = entry.input = input2;
            }
            return e;
          });
          updated();
        }
      };
    },
    async resolveTags() {
      const resolveCtx = { tags: [], entries: [...entries] };
      await hooks.callHook("entries:resolve", resolveCtx);
      for (const entry of resolveCtx.entries) {
        const resolved = entry.resolvedInput || entry.input;
        entry.resolvedInput = await (entry.transform ? entry.transform(resolved) : resolved);
        if (entry.resolvedInput) {
          for (const tag of await shared.normaliseEntryTags(entry)) {
            const tagCtx = { tag, entry, resolvedOptions: head.resolvedOptions };
            await hooks.callHook("tag:normalise", tagCtx);
            resolveCtx.tags.push(tagCtx.tag);
          }
        }
      }
      await hooks.callHook("tags:beforeResolve", resolveCtx);
      await hooks.callHook("tags:resolve", resolveCtx);
      await hooks.callHook("tags:afterResolve", resolveCtx);
      return resolveCtx.tags;
    },
    ssr
  };
  [
    DedupePlugin,
    PayloadPlugin,
    EventHandlersPlugin,
    HashKeyedPlugin,
    SortPlugin,
    TemplateParamsPlugin,
    TitleTemplatePlugin,
    XSSPlugin,
    ...options?.plugins || []
  ].forEach((p) => head.use(p));
  head.hooks.callHook("init", head);
  return head;
}

// @__NO_SIDE_EFFECTS__
function HashHydrationPlugin() {
  return shared.defineHeadPlugin({});
}

const importRe = /@import/;
// @__NO_SIDE_EFFECTS__
function CapoPlugin(options) {
  return shared.defineHeadPlugin({
    hooks: {
      "tags:beforeResolve": function({ tags }) {
        for (const tag of tags) {
          if (tag.tagPosition && tag.tagPosition !== "head")
            continue;
          tag.tagPriority = tag.tagPriority || shared.tagWeight(tag);
          if (tag.tagPriority !== 100)
            continue;
          const isTruthy = (val) => val === "" || val === true;
          const isScript = tag.tag === "script";
          const isLink = tag.tag === "link";
          if (isScript && isTruthy(tag.props.async)) {
            tag.tagPriority = 30;
          } else if (tag.tag === "style" && tag.innerHTML && importRe.test(tag.innerHTML)) {
            tag.tagPriority = 40;
          } else if (isScript && tag.props.src && !isTruthy(tag.props.defer) && !isTruthy(tag.props.async) && tag.props.type !== "module" && !tag.props.type?.endsWith("json")) {
            tag.tagPriority = 50;
          } else if (isLink && tag.props.rel === "stylesheet" || tag.tag === "style") {
            tag.tagPriority = 60;
          } else if (isLink && ["preload", "modulepreload"].includes(tag.props.rel)) {
            tag.tagPriority = 70;
          } else if (isScript && isTruthy(tag.props.defer) && tag.props.src && !isTruthy(tag.props.async)) {
            tag.tagPriority = 80;
          } else if (isLink && ["prefetch", "dns-prefetch", "prerender"].includes(tag.props.rel)) {
            tag.tagPriority = 90;
          }
        }
        options?.track && tags.push({
          tag: "htmlAttrs",
          props: {
            "data-capo": ""
          }
        });
      }
    }
  });
}

const unheadComposablesImports = [
  {
    from: "unhead",
    imports: shared.composableNames
  }
];

function getActiveHead() {
  return activeHead;
}

function useHead(input, options = {}) {
  const head = options.head || getActiveHead();
  return head?.push(input, options);
}

function useHeadSafe(input, options = {}) {
  return useHead(input, {
    ...options || {},
    transform: shared.whitelistSafeInput
  });
}

function useServerHead(input, options = {}) {
  return useHead(input, { ...options, mode: "server" });
}

function useServerHeadSafe(input, options = {}) {
  return useHeadSafe(input, { ...options, mode: "server" });
}

function useSeoMeta(input, options) {
  const { title, titleTemplate, ...meta } = input;
  return useHead({
    title,
    titleTemplate,
    // we need to input the meta so the reactivity will be resolved
    // @ts-expect-error runtime type
    _flatMeta: meta
  }, {
    ...options,
    transform(t) {
      const meta2 = shared.unpackMeta({ ...t._flatMeta });
      delete t._flatMeta;
      return {
        // @ts-expect-error runtime type
        ...t,
        meta: meta2
      };
    }
  });
}

function useServerSeoMeta(input, options) {
  return useSeoMeta(input, {
    ...options || {},
    mode: "server"
  });
}

function useScript(_input, _options) {
  const input = typeof _input === "string" ? { src: _input } : _input;
  const options = _options || {};
  const head = options.head || getActiveHead();
  if (!head)
    throw new Error("Missing Unhead context.");
  const id = input.key || shared.hashCode(input.src || (typeof input.innerHTML === "string" ? input.innerHTML : ""));
  if (head._scripts?.[id])
    return head._scripts[id];
  options.beforeInit?.();
  const syncStatus = (s) => {
    script.status = s;
    head.hooks.callHook(`script:updated`, hookCtx);
  };
  const trigger = typeof options.trigger !== "undefined" ? options.trigger : "client";
  shared.ScriptNetworkEvents.forEach((fn) => {
    const _fn = typeof input[fn] === "function" ? input[fn].bind(options.eventContext) : null;
    input[fn] = (e) => {
      syncStatus(fn === "onload" ? "loaded" : fn === "onerror" ? "error" : "loading");
      _fn?.(e);
    };
  });
  const proxy = { instance: !head.ssr && options?.use?.() || {} };
  const loadPromise = new Promise((resolve, reject) => {
    const emit = (api) => requestAnimationFrame(() => resolve(api));
    const _ = head.hooks.hook("script:updated", ({ script: script2 }) => {
      if (script2.id === id && (script2.status === "loaded" || script2.status === "error")) {
        if (script2.status === "loaded") {
          if (typeof options.use === "function") {
            const api = options.use();
            api && emit(api);
          } else {
            emit({});
          }
        } else if (script2.status === "error") {
          reject(new Error(`Failed to load script: ${input.src}`));
        }
        _();
      }
    });
  }).then((api) => proxy.instance = api);
  const script = {
    id,
    status: "awaitingLoad",
    remove() {
      if (script.entry) {
        script.entry.dispose();
        syncStatus("removed");
        delete head._scripts?.[id];
        return true;
      }
      return false;
    },
    load() {
      if (!script.entry) {
        syncStatus("loading");
        const defaults = {
          defer: true,
          fetchpriority: "low"
        };
        if (input.src && (input.src.startsWith("http") || input.src.startsWith("//"))) {
          defaults.crossorigin = "anonymous";
          defaults.referrerpolicy = "no-referrer";
        }
        script.entry = head.push({
          script: [{ ...defaults, ...input, key: `script.${id}` }]
        }, options);
      }
      return loadPromise;
    }
  };
  const hookCtx = { script };
  if (trigger === "client" && !head.ssr || trigger === "server" && head.ssr)
    script.load();
  else if (trigger instanceof Promise)
    trigger.then(script.load);
  else if (typeof trigger === "function")
    trigger(async () => script.load());
  proxy.$script = Object.assign(loadPromise, script);
  const instance = new Proxy(proxy, {
    get({ instance: _ }, k) {
      const stub = options.stub?.({ script: proxy.$script, fn: k });
      if (stub)
        return stub;
      if (k === "$script")
        return proxy.$script;
      const exists = _ && k in _ && typeof _[k] !== "undefined";
      head.hooks.callHook("script:instance-fn", { script, fn: k, exists });
      return exists ? Reflect.get(_, k) : (...args) => loadPromise.then((api) => {
        const _k = Reflect.get(api, k);
        return typeof _k === "function" ? Reflect.apply(api[k], api, args) : _k;
      });
    }
  });
  head._scripts = Object.assign(
    head._scripts || {},
    { [id]: instance }
  );
  return instance;
}

exports.composableNames = shared.composableNames;
exports.CapoPlugin = CapoPlugin;
exports.HashHydrationPlugin = HashHydrationPlugin;
exports.createHead = createHead;
exports.createHeadCore = createHeadCore;
exports.createServerHead = createServerHead;
exports.getActiveHead = getActiveHead;
exports.unheadComposablesImports = unheadComposablesImports;
exports.useHead = useHead;
exports.useHeadSafe = useHeadSafe;
exports.useScript = useScript;
exports.useSeoMeta = useSeoMeta;
exports.useServerHead = useServerHead;
exports.useServerHeadSafe = useServerHeadSafe;
exports.useServerSeoMeta = useServerSeoMeta;
