import { $inject, Alepha } from "alepha";
import type { Head, HeadMeta } from "../interfaces/Head.ts";
import { HeadProvider } from "./HeadProvider.ts";

/**
 * Browser-side head provider that manages document head elements.
 *
 * Used by ReactBrowserProvider and ReactBrowserRouterProvider to update
 * document title, meta tags, and other head elements during client-side
 * navigation.
 */
export class BrowserHeadProvider {
  protected readonly alepha = $inject(Alepha);
  protected readonly headProvider = $inject(HeadProvider);

  protected get document(): Document {
    return window.document;
  }

  /**
   * Fill head state from route configurations and render to document.
   * Combines fillHead from HeadProvider with renderHead to the DOM.
   *
   * Only runs in browser environment - no-op on server.
   */
  public fillAndRenderHead(state: { head: Head; layers: Array<any> }): void {
    // Skip on server-side
    if (!this.alepha.isBrowser()) {
      return;
    }

    this.headProvider.fillHead(state as any);
    if (state.head) {
      this.renderHead(this.document, state.head);
    }
  }

  /**
   * Re-evaluate all global $head entries and apply the result to the DOM.
   *
   * Call this when something that affects global $head output changes at runtime
   * (e.g., theme switch). Page-level head (title, meta from routes) is not touched.
   */
  public refreshGlobalHead(): void {
    const head = this.headProvider.resolveGlobal();
    this.renderHead(this.document, head);
  }

  public getHead(document: Document): Head {
    return {
      get title() {
        return document.title;
      },
      get htmlAttributes() {
        const attrs: Record<string, string> = {};
        for (const attr of document.documentElement.attributes) {
          attrs[attr.name] = attr.value;
        }
        return attrs;
      },
      get bodyAttributes() {
        const attrs: Record<string, string> = {};
        for (const attr of document.body.attributes) {
          attrs[attr.name] = attr.value;
        }
        return attrs;
      },
      get meta() {
        const metas: HeadMeta[] = [];
        // Get meta tags with name attribute
        for (const meta of document.head.querySelectorAll("meta[name]")) {
          const name = meta.getAttribute("name");
          const content = meta.getAttribute("content");
          if (name && content) {
            metas.push({ name, content });
          }
        }
        // Get meta tags with property attribute (OpenGraph)
        for (const meta of document.head.querySelectorAll("meta[property]")) {
          const property = meta.getAttribute("property");
          const content = meta.getAttribute("content");
          if (property && content) {
            metas.push({ property, content });
          }
        }
        return metas;
      },
    };
  }

  public renderHead(document: Document, head: Head): void {
    if (head.title) {
      document.title = head.title;
    }

    if (head.bodyAttributes) {
      for (const [key, value] of Object.entries(head.bodyAttributes)) {
        if (value) {
          document.body.setAttribute(key, value);
        } else {
          document.body.removeAttribute(key);
        }
      }
    }

    if (head.htmlAttributes) {
      for (const [key, value] of Object.entries(head.htmlAttributes)) {
        if (value) {
          document.documentElement.setAttribute(key, value);
        } else {
          document.documentElement.removeAttribute(key);
        }
      }
    }

    if (head.meta) {
      for (const it of head.meta) {
        this.renderMetaTag(document, it);
      }
    }

    if (head.link) {
      for (const it of head.link) {
        const { rel, href } = it;
        let link = document.querySelector(`link[rel="${rel}"][href="${href}"]`);
        if (!link) {
          link = document.createElement("link");
          link.setAttribute("rel", rel);
          link.setAttribute("href", href);
          if (it.type) {
            link.setAttribute("type", it.type);
          }
          if (it.as) {
            link.setAttribute("as", it.as);
          }
          if (it.crossorigin != null) {
            link.setAttribute("crossorigin", "");
          }
          if (it.media) {
            link.setAttribute("media", it.media);
          }
          if (it.sizes) {
            link.setAttribute("sizes", it.sizes);
          }
          if (it.hreflang) {
            link.setAttribute("hreflang", it.hreflang);
          }
          document.head.appendChild(link);
        }
      }
    }

    if (head.script) {
      for (const it of head.script) {
        this.renderScriptTag(document, it);
      }
    }
  }

  protected renderScriptTag(
    document: Document,
    script:
      | string
      | (Record<string, string | boolean | undefined> & { content?: string }),
  ): void {
    // Plain string → inline script. Dedupe by exact content match against
    // any existing inline script (handles SSR-emitted globals that would
    // otherwise be re-appended on hydration).
    if (typeof script === "string") {
      if (this.findInlineScriptByContent(document, script)) return;
      const el = document.createElement("script");
      el.textContent = script;
      document.head.appendChild(el);
      return;
    }

    const { content, ...attrs } = script;

    // src-based scripts: dedupe by src attribute (existing behaviour).
    if (attrs.src) {
      if (document.querySelector(`script[src="${attrs.src}"]`)) return;
    } else if (typeof attrs.id === "string") {
      // id-based dedupe — single source of truth per id.
      if (document.querySelector(`script#${CSS.escape(attrs.id)}`)) return;
    } else if (content) {
      // Inline scripts with `content` and no src/id: fall back to content match.
      if (this.findInlineScriptByContent(document, content)) return;
    }

    const el = document.createElement("script");
    for (const [key, value] of Object.entries(attrs)) {
      if (value === true) {
        el.setAttribute(key, "");
      } else if (value !== undefined && value !== false) {
        el.setAttribute(key, String(value));
      }
    }
    if (content) {
      el.textContent = content;
    }
    document.head.appendChild(el);
  }

  /**
   * Find an existing inline `<script>` tag (no `src`) with matching textContent.
   * Used to make `renderScriptTag` idempotent across hydration + navigation,
   * so SSR-emitted global scripts aren't re-appended client-side.
   */
  protected findInlineScriptByContent(
    document: Document,
    content: string,
  ): Element | null {
    for (const existing of document.head.querySelectorAll(
      "script:not([src])",
    )) {
      if (existing.textContent === content) return existing;
    }
    return null;
  }

  protected renderMetaTag(document: Document, meta: HeadMeta): void {
    const { content } = meta;

    // Handle OpenGraph tags (property attribute)
    if (meta.property) {
      const existing = document.querySelector(
        `meta[property="${meta.property}"]`,
      );
      if (existing) {
        existing.setAttribute("content", content);
      } else {
        const newMeta = document.createElement("meta");
        newMeta.setAttribute("property", meta.property);
        newMeta.setAttribute("content", content);
        document.head.appendChild(newMeta);
      }
      return;
    }

    // Handle standard meta tags (name attribute)
    if (meta.name) {
      const existing = document.querySelector(`meta[name="${meta.name}"]`);
      if (existing) {
        existing.setAttribute("content", content);
      } else {
        const newMeta = document.createElement("meta");
        newMeta.setAttribute("name", meta.name);
        newMeta.setAttribute("content", content);
        document.head.appendChild(newMeta);
      }
    }
  }
}
