// Global state for tracking the current reactive effect
let activeEffect = null;
const effectQueue = new Set();
let isFlushScheduled = false;
let flushCount = 0;

const flushEffectQueue = () => {
  isFlushScheduled = false;
  flushCount++;

  if (flushCount > 100) {
    effectQueue.clear();
    flushCount = 0;
    throw new Error("SigPro: Infinite reactive loop detected.");
  }

  try {
    const effects = Array.from(effectQueue);
    effectQueue.clear();
    for (const effect of effects) effect.run();
  } catch (error) {
    console.error("SigPro Flush Error:", error);
  } finally {
    setTimeout(() => {
      flushCount = 0;
    }, 0);
  }
};

/**
 * Creates a reactive signal
 * @param {any} initialValue - Initial value or getter function
 * @returns {Function} Signal getter/setter function
 */
const $ = (initialValue) => {
  const subscribers = new Set();
  let signal;

  if (typeof initialValue === "function") {
    let isDirty = true;
    let cachedValue;

    const computedEffect = {
      dependencies: new Set(),
      markDirty: () => {
        if (!isDirty) {
          isDirty = true;
          subscribers.forEach((sub) => {
            if (sub.markDirty) sub.markDirty();
            effectQueue.add(sub);
          });
          if (!isFlushScheduled && effectQueue.size) {
            isFlushScheduled = true;
            queueMicrotask(flushEffectQueue);
          }
        }
      },
      run: () => {
        computedEffect.dependencies.forEach((dep) => dep.delete(computedEffect));
        computedEffect.dependencies.clear();
        const prev = activeEffect;
        activeEffect = computedEffect;
        try {
          cachedValue = initialValue();
        } finally {
          activeEffect = prev;
          isDirty = false;
        }
      },
    };

    signal = () => {
      if (activeEffect) {
        subscribers.add(activeEffect);
        activeEffect.dependencies.add(subscribers);
      }
      if (isDirty) computedEffect.run();
      return cachedValue;
    };
  } else {
    signal = (...args) => {
      if (args.length) {
        const next = typeof args[0] === "function" ? args[0](initialValue) : args[0];
        if (!Object.is(initialValue, next)) {
          initialValue = next;
          subscribers.forEach((sub) => {
            if (sub.markDirty) sub.markDirty();
            effectQueue.add(sub);
          });
          if (!isFlushScheduled && effectQueue.size) {
            isFlushScheduled = true;
            queueMicrotask(flushEffectQueue);
          }
        }
      }
      if (activeEffect) {
        subscribers.add(activeEffect);
        activeEffect.dependencies.add(subscribers);
      }
      return initialValue;
    };
  }
  return signal;
};

let currentPageCleanups = null;

/**
 * Creates a reactive effect that runs when dependencies change
 * @param {Function} effectFn - The effect function to run
 * @returns {Function} Cleanup function to stop the effect
 */
const $e = (effectFn) => {
  const effect = {
    dependencies: new Set(),
    cleanupHandlers: new Set(),
    run() {
      this.cleanupHandlers.forEach((h) => h());
      this.cleanupHandlers.clear();
      this.dependencies.forEach((dep) => dep.delete(this));
      this.dependencies.clear();

      const prev = activeEffect;
      activeEffect = this;
      try {
        const res = effectFn();
        if (typeof res === "function") this.cleanupHandlers.add(res);
      } finally {
        activeEffect = prev;
      }
    },
    stop() {
      this.cleanupHandlers.forEach((h) => h());
      this.dependencies.forEach((dep) => dep.delete(this));
    },
  };

  if (currentPageCleanups) currentPageCleanups.push(() => effect.stop());
  if (activeEffect) activeEffect.cleanupHandlers.add(() => effect.stop());

  effect.run();
  return () => effect.stop();
};

/**
 * Persistent signal with localStorage
 * @param {string} key - Storage key
 * @param {any} initialValue - Default value if none stored
 * @param {Storage} [storage=localStorage] - Storage type (localStorage/sessionStorage)
 * @returns {Function} Signal that persists to storage
 */
const $s = (key, initialValue, storage = localStorage) => {
  let initial;
  try {
    const saved = storage.getItem(key);
    if (saved !== null) {
      initial = JSON.parse(saved);
    } else {
      initial = initialValue;
    }
  } catch (e) {
    console.warn(`Error reading ${key} from storage:`, e);
    initial = initialValue;
    storage.removeItem(key);
  }

  const signal = $(initial);

  $e(() => {
    try {
      const value = signal();
      if (value === undefined || value === null) {
        storage.removeItem(key);
      } else {
        storage.setItem(key, JSON.stringify(value));
      }
    } catch (e) {
      console.warn(`Error saving ${key} to storage:`, e);
    }
  });

  return signal;
};

/**
 * Tagged template literal for creating reactive HTML
 * @param {string[]} strings - Template strings
 * @param {...any} values - Dynamic values
 * @returns {DocumentFragment} Reactive document fragment
 * @see {@link https://developer.mozilla.org/es/docs/Glossary/Cross-site_scripting}
 */
const html = (strings, ...values) => {
  const templateCache = html._templateCache ?? (html._templateCache = new WeakMap());

  const getNodeByPath = (root, path) => path.reduce((node, index) => node?.childNodes?.[index], root);

  const applyTextContent = (node, values) => {
    const parts = node.textContent.split("{{part}}");
    const parent = node.parentNode;
    let valueIndex = 0;

    parts.forEach((part, index) => {
      if (part) parent.insertBefore(document.createTextNode(part), node);
      if (index < parts.length - 1) {
        const currentValue = values[valueIndex++];
        const startMarker = document.createComment("s");
        const endMarker = document.createComment("e");
        parent.insertBefore(startMarker, node);
        parent.insertBefore(endMarker, node);

        if (typeof currentValue === "function") {
          let lastResult;
          $e(() => {
            const result = currentValue();
            if (result === lastResult) return;
            lastResult = result;
            updateContent(result);
          });
        } else {
          updateContent(currentValue);
        }

        function updateContent(result) {
          if (typeof result !== "object" && !Array.isArray(result)) {
            const textNode = startMarker.nextSibling;
            const safeText = String(result ?? "");

            if (textNode !== endMarker && textNode?.nodeType === 3) {
              textNode.textContent = safeText;
            } else {
              while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling);
              parent.insertBefore(document.createTextNode(safeText), endMarker);
            }
          } else {
            while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling);

            const items = Array.isArray(result) ? result : [result];
            const fragment = document.createDocumentFragment();
            items.forEach((item) => {
              if (item == null || item === false) return;
              const nodeItem = item instanceof Node ? item : document.createTextNode(item);
              fragment.appendChild(nodeItem);
            });
            parent.insertBefore(fragment, endMarker);
          }
        }
      }
    });
    node.remove();
  };

  let cachedTemplate = templateCache.get(strings);
  if (!cachedTemplate) {
    const template = document.createElement("template");
    template.innerHTML = strings.join("{{part}}");

    const dynamicNodes = [];
    const treeWalker = document.createTreeWalker(template.content, 133);

    const getNodePath = (node) => {
      const path = [];
      while (node && node !== template.content) {
        let index = 0;
        for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) index++;
        path.push(index);
        node = node.parentNode;
      }
      return path.reverse();
    };

    let currentNode;
    while ((currentNode = treeWalker.nextNode())) {
      let isDynamic = false;
      const nodeInfo = {
        type: currentNode.nodeType,
        path: getNodePath(currentNode),
        parts: [],
      };

      if (currentNode.nodeType === 1) {
        for (let i = 0; i < currentNode.attributes.length; i++) {
          const attribute = currentNode.attributes[i];
          if (attribute.value.includes("{{part}}")) {
            nodeInfo.parts.push({ name: attribute.name });
            isDynamic = true;
          }
        }
      } else if (currentNode.nodeType === 3 && currentNode.textContent.includes("{{part}}")) {
        isDynamic = true;
      }

      if (isDynamic) dynamicNodes.push(nodeInfo);
    }

    templateCache.set(strings, (cachedTemplate = { template, dynamicNodes }));
  }

  const fragment = cachedTemplate.template.content.cloneNode(true);
  let valueIndex = 0;

  const targets = cachedTemplate.dynamicNodes.map((nodeInfo) => ({
    node: getNodeByPath(fragment, nodeInfo.path),
    info: nodeInfo,
  }));

  targets.forEach(({ node, info }) => {
    if (!node) return;

    if (info.type === 1) {
      info.parts.forEach((part) => {
        const currentValue = values[valueIndex++];
        const attributeName = part.name;
        const firstChar = attributeName[0];

        if (firstChar === "@") {
          const [eventName, ...modifiers] = attributeName.slice(1).split(".");

          const handlerWrapper = (e) => {
            if (modifiers.includes("prevent")) e.preventDefault();
            if (modifiers.includes("stop")) e.stopPropagation();
            if (modifiers.includes("self") && e.target !== node) return;

            if (modifiers.some((m) => m.startsWith("debounce"))) {
              const ms = modifiers.find((m) => m.startsWith("debounce"))?.split(":")[1] || 300;
              clearTimeout(node._debounceTimer);
              node._debounceTimer = setTimeout(() => currentValue(e), ms);
              return;
            }

            if (modifiers.includes("once")) {
              node.removeEventListener(eventName, handlerWrapper);
            }

            currentValue(e);
          };

          node.addEventListener(eventName, handlerWrapper, {
            passive: modifiers.includes("passive"),
            capture: modifiers.includes("capture"),
          });
        } else if (firstChar === ":") {
          const propertyName = attributeName.slice(1);
          const eventType = node.type === "checkbox" || node.type === "radio" ? "change" : "input";

          if (typeof currentValue === "function") {
            $e(() => {
              const value = currentValue();
              if (node[propertyName] !== value) node[propertyName] = value;
            });
          } else {
            node[propertyName] = currentValue;
          }

          node.addEventListener(eventType, () => {
            const value = eventType === "change" ? node.checked : node.value;
            if (typeof currentValue === "function") currentValue(value);
          });
        } else if (firstChar === "?") {
          const attrName = attributeName.slice(1);

          if (typeof currentValue === "function") {
            $e(() => {
              const result = currentValue();
              node.toggleAttribute(attrName, !!result);
            });
          } else {
            node.toggleAttribute(attrName, !!currentValue);
          }
        } else if (firstChar === ".") {
          const propertyName = attributeName.slice(1);

          if (typeof currentValue === "function") {
            $e(() => {
              const result = currentValue();
              node[propertyName] = result;
              if (result != null && typeof result !== "object" && typeof result !== "boolean") {
                node.setAttribute(propertyName, result);
              }
            });
          } else {
            node[propertyName] = currentValue;
            if (currentValue != null && typeof currentValue !== "object" && typeof currentValue !== "boolean") {
              node.setAttribute(propertyName, currentValue);
            }
          }
        } else {
          if (typeof currentValue === "function") {
            $e(() => node.setAttribute(attributeName, currentValue()));
          } else {
            node.setAttribute(attributeName, currentValue);
          }
        }
      });
    } else if (info.type === 3) {
      const placeholderCount = node.textContent.split("{{part}}").length - 1;
      applyTextContent(node, values.slice(valueIndex, valueIndex + placeholderCount));
      valueIndex += placeholderCount;
    }
  });

  return fragment;
};

/**
 * Creates a page with automatic cleanup
 * @param {Function} setupFunction - Page setup function that receives props
 * @returns {Function} A function that creates page instances with props
 */
const $p = (setupFunction) => {
  const tagName = "page-" + Math.random().toString(36).substring(2, 9);

  customElements.define(
    tagName,
    class extends HTMLElement {
      connectedCallback() {
        this.style.display = "contents";
        this._cleanups = [];
        currentPageCleanups = this._cleanups;

        try {
          const result = setupFunction({
            params: JSON.parse(this.getAttribute("params") || "{}"),
            onUnmount: (fn) => this._cleanups.push(fn),
          });
          this.appendChild(result instanceof Node ? result : document.createTextNode(String(result)));
        } finally {
          currentPageCleanups = null;
        }
      }

      disconnectedCallback() {
        this._cleanups.forEach((fn) => fn());
        this._cleanups = [];
        this.innerHTML = "";
      }
    },
  );

  return (props = {}) => {
    const el = document.createElement(tagName);
    el.setAttribute("params", JSON.stringify(props));
    return el;
  };
};

/**
 * Creates a custom web component with reactive properties
 * @param {string} tagName - Custom element tag name
 * @param {Function} setupFunction - Component setup function
 * @param {string[]} observedAttributes - Array of observed attributes
 * @param {boolean} useShadowDOM - Enable Shadow DOM (default: false)
 */
const $c = (tagName, setupFunction, observedAttributes = [], useShadowDOM = false) => {
  if (customElements.get(tagName)) return;

  customElements.define(
    tagName,
    class extends HTMLElement {
      static get observedAttributes() {
        return observedAttributes;
      }

      constructor() {
        super();
        this._propertySignals = {};
        this.cleanupFunctions = [];

        if (useShadowDOM) {
          this._root = this.attachShadow({ mode: "open" });
        } else {
          this._root = this;
        }

        observedAttributes.forEach((attr) => (this._propertySignals[attr] = $(undefined)));
      }

      connectedCallback() {
        const frozenChildren = [...this.childNodes];
        this._root.innerHTML = "";

        observedAttributes.forEach((attr) => {
          const initialValue = this.hasOwnProperty(attr) ? this[attr] : this.getAttribute(attr);

          Object.defineProperty(this, attr, {
            get: () => this._propertySignals[attr](),
            set: (value) => {
              const processedValue = value === "false" ? false : value === "" && attr !== "value" ? true : value;
              this._propertySignals[attr](processedValue);
            },
            configurable: true,
          });

          if (initialValue !== null && initialValue !== undefined) this[attr] = initialValue;
        });

        const context = {
          select: (selector) => this._root.querySelector(selector),
          selectAll: (selector) => this._root.querySelectorAll(selector),
          slot: (name) =>
            frozenChildren.filter((node) => {
              const slotName = node.nodeType === 1 ? node.getAttribute("slot") : null;
              return name ? slotName === name : !slotName;
            }),
          emit: (name, detail) => this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })),
          host: this,
          root: this._root,
          onUnmount: (cleanupFn) => this.cleanupFunctions.push(cleanupFn),
        };

        const result = setupFunction(this._propertySignals, context);
        if (result instanceof Node) this._root.appendChild(result);
      }

      attributeChangedCallback(name, oldValue, newValue) {
        if (this[name] !== newValue) this[name] = newValue;
      }

      disconnectedCallback() {
        this.cleanupFunctions.forEach((cleanupFn) => cleanupFn());
        this.cleanupFunctions = [];
      }
    },
  );
};

/**
 * Ultra-simple fetch wrapper with optional loading signal
 * @param {string} url - Endpoint URL
 * @param {Object} data - Data to send (automatically JSON.stringify'd)
 * @param {Function} [loading] - Optional signal function to track loading state
 * @returns {Promise<Object|null>} Parsed JSON response or null on error
 */
const $f = async (url, data, loading) => {
  if (loading) loading(true);

  try {
    const res = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });

    const text = await res.text();
    try {
      return JSON.parse(text);
    } catch (e) {
      console.warn("Invalid JSON response");
      return null;
    }
  } catch (e) {
    return null;
  } finally {
    if (loading) loading(false);
  }
};

/**
 * Creates a router for hash-based navigation
 * @param {Array<{path: string|RegExp, component: Function}>} routes - Route configurations
 * @returns {HTMLDivElement} Router container element
 */
const $r = (routes) => {
  const getCurrentPath = () => window.location.hash.replace(/^#/, "") || "/";
  const container = document.createElement("div");
  container.style.display = "contents";

  const matchRoute = (path, routePath) => {
    if (!routePath.includes(":")) {
      return routePath === path ? {} : null;
    }

    const parts = routePath.split("/");
    const pathParts = path.split("/");

    if (parts.length !== pathParts.length) return null;

    const params = {};
    for (let i = 0; i < parts.length; i++) {
      if (parts[i].startsWith(":")) {
        params[parts[i].slice(1)] = pathParts[i];
      } else if (parts[i] !== pathParts[i]) {
        return null;
      }
    }
    return params;
  };

  const render = () => {
    const path = getCurrentPath();
    let matchedRoute = null;
    let routeParams = {};

    for (const route of routes) {
      const params = matchRoute(path, route.path);
      if (params !== null) {
        matchedRoute = route;
        routeParams = params;
        break;
      }
    }

    const view = matchedRoute ? matchedRoute.component(routeParams) : Object.assign(document.createElement("h1"), { textContent: "404" });

    container.replaceChildren(view instanceof Node ? view : document.createTextNode(String(view ?? "")));
  };

  window.addEventListener("hashchange", render);
  render();
  return container;
};

$r.go = (path) => {
  const targetPath = path.startsWith("/") ? path : `/${path}`;
  if (window.location.hash !== `#${targetPath}`) {
    window.location.hash = targetPath;
  }
};

/* Can customize the name of your functions */

$.effect = $e;
$.page = $p;
$.component = $c;
$.fetch = $f;
$.router = $r;
$.storage = $s;

if (typeof window !== "undefined") {
  window.$ = $;
}
export { $, html };
