/**
 * AdiaUI Router — Self-contained vanilla JS bundle.
 * Includes: RouteController + router-ui component.
 *
 * Depends on: core.js (UIElement), controllers.js (BaseController)
 *
 * Usage:
 *   import { UIElement } from './core.js';
 *   import { UIRouter, RouteController } from './router.js';
 *
 *   // Option 1: Declarative routes
 *   const router = document.querySelector('router-ui');
 *   router.routes = [
 *     { path: '/', content: '/pages/home.html', title: 'Home' },
 *     { path: '/about', content: '/pages/about.html', title: 'About' },
 *     { path: '/users/:id', content: '/pages/user.html', title: 'User' },
 *   ];
 *
 *   // Option 2: Controller directly
 *   const ctrl = new RouteController({ routes: [...] });
 *   router.controller = ctrl;
 *   ctrl.commands.navigate('/about');
 */

import { UIElement } from './element.js';
import { BaseController } from './controller.js';

// ═══════════════════════════════════════════════════════════════
// ADIA PROVIDER (inline — tiny base class)
// ═══════════════════════════════════════════════════════════════

class UIProvider extends UIElement {
  static template = () => null;
  #abort = null;
  get abort() { if (!this.#abort) this.#abort = new AbortController(); return this.#abort; }
  get signal() { return this.abort.signal; }
  emit(name, detail) {
    this.dispatchEvent(detail !== undefined
      ? new CustomEvent(name, { bubbles: true, detail })
      : new Event(name, { bubbles: true })
    );
  }
  disconnected() { this.#abort?.abort(); this.#abort = null; }
}

// ═══════════════════════════════════════════════════════════════
// ROUTE CONTROLLER
// ═══════════════════════════════════════════════════════════════

export class RouteController extends BaseController {
  static schema = {
    name: 'route',
    state: { path: 'string', params: 'object', route: 'object', previous: 'string' },
    commands: ['navigate', 'replace', 'back', 'forward', 'setRoutes'],
    attributes: ['data-route-path'],
  };

  #routes = [];
  #path = '';
  #previous = '';
  #params = {};
  #route = null;
  #historySync = true;
  #boundPopState = null;

  constructor({ routes = [], initial, historySync = true } = {}) {
    super();
    this.#routes = routes;
    this.#historySync = historySync;
    this.#path = initial ?? location.pathname;
    this.#match();
  }

  getState() {
    return {
      path: this.#path,
      params: { ...this.#params },
      route: this.#route,
      previous: this.#previous,
    };
  }

  reflect() {
    const host = this.host;
    if (!host) return;
    host.setAttribute('data-route-path', this.#path);
  }

  onConnect(host) {
    if (this.#historySync) {
      this.#boundPopState = () => {
        this.#previous = this.#path;
        this.#path = location.pathname;
        this.#match();
        this.notify();
      };
      window.addEventListener('popstate', this.#boundPopState);
    }
  }

  onDisconnect() {
    if (this.#boundPopState) {
      window.removeEventListener('popstate', this.#boundPopState);
      this.#boundPopState = null;
    }
  }

  #match() {
    this.#params = {};
    this.#route = null;
    for (const route of this.#routes) {
      const match = this.#matchPath(route.path, this.#path);
      if (match) {
        this.#params = match.params;
        this.#route = route;
        return;
      }
    }
  }

  #matchPath(pattern, path) {
    const patternParts = pattern.split('/').filter(Boolean);
    const pathParts = path.split('/').filter(Boolean);
    if (!pattern.includes(':')) return pattern === path ? { params: {} } : null;
    if (patternParts.length !== pathParts.length) return null;
    const params = {};
    for (let i = 0; i < patternParts.length; i++) {
      if (patternParts[i].startsWith(':')) params[patternParts[i].slice(1)] = pathParts[i];
      else if (patternParts[i] !== pathParts[i]) return null;
    }
    return { params };
  }

  commands = {
    navigate: (path) => {
      if (path === this.#path) return;
      this.#previous = this.#path;
      this.#path = path;
      this.#match();
      if (this.#historySync) history.pushState(null, '', path);
      this.notify();
    },
    replace: (path) => {
      this.#previous = this.#path;
      this.#path = path;
      this.#match();
      if (this.#historySync) history.replaceState(null, '', path);
      this.notify();
    },
    back: () => { if (this.#historySync) history.back(); },
    forward: () => { if (this.#historySync) history.forward(); },
    setRoutes: (routes) => {
      this.#routes = routes;
      this.#match();
      this.notify();
    },
  };
}

// ═══════════════════════════════════════════════════════════════
// ROUTER-UI — content fragment renderer
// ═══════════════════════════════════════════════════════════════

export class UIRouter extends UIProvider {
  #cache = new Map();
  #ownCtrl = null;
  #lastPath = null;

  get #ctrl() { return this.#ownCtrl || this.controller; }

  set routes(list) {
    if (!this.#ownCtrl) {
      this.#ownCtrl = new RouteController({ routes: list });
      this.controller = this.#ownCtrl;
      this.#ownCtrl.notify();
    } else {
      this.#ownCtrl.commands.setRoutes(list);
    }
  }

  navigate(path) { this.#ctrl?.commands.navigate(path); }
  replace(path) { this.#ctrl?.commands.replace(path); }

  connected() {
    super.connected();
    document.addEventListener('click', this.#onClick);
  }

  disconnected() {
    super.disconnected();
    document.removeEventListener('click', this.#onClick);
  }

  #onClick = (e) => {
    const ctrl = this.#ctrl;
    if (!ctrl) return;
    const a = e.target.closest('a[href]');
    if (!a) return;
    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
    if (a.target && a.target !== '_self') return;
    const href = a.getAttribute('href');
    if (href.startsWith('http') || href.startsWith('//') || href.startsWith('#')) return;
    e.preventDefault();
    ctrl.commands.navigate(href);
  };

  render() {
    const ctrl = this.#ctrl;
    if (!ctrl) return;
    const { path, route } = ctrl.getState();
    if (path === this.#lastPath) return;
    this.#lastPath = path;
    if (route?.content) this.#loadContent(route, ctrl);
  }

  async #loadContent(route, ctrl) {
    let html;
    if (this.#cache.has(route.content)) {
      html = this.#cache.get(route.content);
    } else {
      try {
        const res = await fetch(route.content, { signal: this.signal });
        if (!res.ok) return;
        html = await res.text();
        this.#cache.set(route.content, html);
      } catch (e) {
        if (e.name !== 'AbortError') console.error('Router fetch error:', e);
        return;
      }
    }

    // Template resolver hook — lets the site wrap content in a template before injection
    if (this.templateResolver) {
      html = await this.templateResolver(html, route);
    }

    this.innerHTML = html;
    // Re-stamp executable <script> tags so consumer demos can wire data via
    // imperative JS APIs (the documented `el.items = […]` / `el.invoice = {…}`
    // pattern used by plan-picker, invoice-detail, list-window et al). HTML
    // assigned via innerHTML does NOT execute embedded <script> elements per
    // the HTML spec; we replicate the canonical standalone-wrapper rehydration
    // pattern by cloning each script + swapping the inert original. `<script
    // type="application/json" …>` blocks are left untouched so the receiving
    // custom element's connectedCallback can absorb them (the documented
    // in-band data pattern; see PlanPicker.#absorbInlinePlansScript et al).
    for (const oldScript of this.querySelectorAll('script')) {
      const type = oldScript.getAttribute('type') || '';
      // Skip data blocks — those are consumed by the runtime, not executed.
      if (type === 'application/json' || type === 'application/ld+json') continue;
      const newScript = document.createElement('script');
      if (type) newScript.type = type;
      if (oldScript.src) newScript.src = oldScript.src;
      else newScript.textContent = oldScript.textContent;
      oldScript.replaceWith(newScript);
    }
    this.scrollTo(0, 0);
    (this.closest('section') || this.parentElement)?.scrollTo(0, 0);

    if (route.title) document.title = route.title;

    // Auto-import setup module
    if (route.content?.endsWith('.content.html')) {
      const setupPath = route.content.replace('.content.html', '.setup.js');
      try {
        const mod = await import(/* @vite-ignore */ setupPath);
        if (mod.default) mod.default(this);
      } catch { /* setup module is optional */ }
    }

    const state = ctrl.getState();
    this.emit('route-loaded', {
      path: state.path,
      params: state.params,
      title: route.title || '',
      section: route.section || '',
    });
  }
}

customElements.define('router-ui', UIRouter);
