/**
 * AdiaUI Controllers — Self-contained vanilla JS bundle.
 * Includes: BaseController, RouteController.
 *
 * Zero dependencies. Drop into CodePen, StackBlitz, or any vanilla project.
 */

// ═══════════════════════════════════════════════════════════════
// BASE CONTROLLER
// ═══════════════════════════════════════════════════════════════

const validated = new WeakSet();

export class BaseController {
  #host = null;
  #subs = new Set();

  get host() { return this.#host; }

  connect(host) {
    const ctor = this.constructor;
    if (!validated.has(ctor) && ctor !== BaseController) {
      validated.add(ctor);
      if (!ctor.schema) {
        console.warn(`AdiaUI: ${ctor.name} extends BaseController without static schema`);
      }
      if (this.getState === BaseController.prototype.getState) {
        console.error(`AdiaUI: ${ctor.schema?.name ?? ctor.name} must implement getState()`);
      }
    }
    if (this.#host && this.#host !== host) {
      console.warn(`AdiaUI: ${ctor.schema?.name ?? ctor.name} already connected to a different host`);
    }
    this.#host = host;
    this.onConnect(host);
    this.reflect();
  }

  disconnect(host) {
    const h = host ?? this.#host;
    if (!h) return;
    this.onDisconnect(h);
    this.#host = null;
  }

  subscribe(fn) {
    this.#subs.add(fn);
    return () => this.#subs.delete(fn);
  }

  notify() {
    this.reflect();
    for (const fn of this.#subs) fn();
  }

  onConnect(host) {}
  onDisconnect(host) {}
  reflect() {}

  getState() {
    throw new Error(`${this.constructor.schema?.name ?? this.constructor.name}: getState() not implemented`);
  }
}

// ═══════════════════════════════════════════════════════════════
// 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();
    },
  };
}
