export interface InterceptRule {
  id: string;
  urlPattern: string;
  action: 'block' | 'mock';
  mockResponse?: {
    status: number;
    headers: Record<string, string>;
    body: string;
  };
}

export interface InterceptorClient {
  evaluate<T = unknown>(expression: string): Promise<T>;
}

let ruleIdCounter = 0;

export function globToRegex(pattern: string): RegExp {
  let regex = '';
  let i = 0;
  while (i < pattern.length) {
    const ch = pattern[i];
    if (ch === '*' && pattern[i + 1] === '*') {
      regex += '.*';
      i += 2;
      if (pattern[i] === '/') i++;
    } else if (ch === '*') {
      regex += '[^/]*';
      i++;
    } else if (ch === '?') {
      regex += '[^/]';
      i++;
    } else if (ch === '.') {
      regex += '\\.';
      i++;
    } else if ('^$+{}[]|()\\'.includes(ch)) {
      regex += '\\' + ch;
      i++;
    } else {
      regex += ch;
      i++;
    }
  }
  return new RegExp('^' + regex + '$');
}

export function matchUrl(url: string, pattern: string): boolean {
  if (!pattern.includes('*') && !pattern.includes('?')) {
    return url.includes(pattern);
  }
  return globToRegex(pattern).test(url);
}

export class NetworkInterceptor {
  private rules: Map<string, InterceptRule> = new Map();
  private _enabled = false;
  private _offline = false;

  get enabled(): boolean { return this._enabled; }
  get offline(): boolean { return this._offline; }

  addRule(rule: Omit<InterceptRule, 'id'>): InterceptRule {
    const id = 'rule_' + (++ruleIdCounter);
    const full: InterceptRule = { id, ...rule };
    this.rules.set(id, full);
    return full;
  }

  removeRule(id: string): boolean { return this.rules.delete(id); }
  listRules(): InterceptRule[] { return [...this.rules.values()]; }
  clearRules(): void { this.rules.clear(); }

  findMatchingRule(url: string): InterceptRule | undefined {
    for (const rule of this.rules.values()) {
      if (matchUrl(url, rule.urlPattern)) return rule;
    }
    return undefined;
  }

  async enable(client: InterceptorClient): Promise<void> {
    this._enabled = true;
    await this.inject(client);
  }

  async disable(client: InterceptorClient): Promise<void> {
    this._enabled = false;
    this._offline = false;
    this.rules.clear();
    await client.evaluate('(function(){if(window.__osOriginalFetch){window.fetch=window.__osOriginalFetch;delete window.__osOriginalFetch}if(window.__osOriginalXHROpen){XMLHttpRequest.prototype.open=window.__osOriginalXHROpen;delete window.__osOriginalXHROpen}if(window.__osOriginalXHRSend){XMLHttpRequest.prototype.send=window.__osOriginalXHRSend;delete window.__osOriginalXHRSend}delete window.__osInterceptRules;delete window.__osOfflineMode})()');
  }

  async setOffline(enabled: boolean, client: InterceptorClient): Promise<void> {
    this._offline = enabled;
    if (enabled) {
      if (!this._enabled) this._enabled = true;
      await this.inject(client);
      return;
    }

    if (!this._enabled) return;
    if (this.rules.size === 0) {
      await this.disable(client);
      return;
    }
    await this.inject(client);
  }

  async syncRules(client: InterceptorClient): Promise<void> {
    if (!this._enabled) return;
    await this.inject(client);
  }

  /**
   * Re-inject the interceptor after a page navigation has wiped the JS
   * context. Callers should hook this into the browser backend's
   * "page loaded" event (WebKit `Page.loadEventFired` / `Page.frameNavigated`).
   * Safe to call when disabled — it's a no-op and returns false.
   */
  async reinject(client: InterceptorClient): Promise<boolean> {
    if (!this._enabled) return false;
    await this.inject(client);
    return true;
  }

  /**
   * Subscribe the interceptor to a page-load event source so it
   * automatically re-injects after every navigation. Returns the
   * unsubscribe function so the caller can detach when tearing down a
   * session. We accept a thin wiring shim rather than importing
   * WebKitClient directly so this module stays test-friendly and usable
   * from non-WebKit backends in the future.
   */
  installReinjectionHook(
    client: InterceptorClient,
    subscribe: (onLoad: () => void) => (() => void) | void,
  ): () => void {
    const unsubscribe = subscribe(() => {
      void this.reinject(client).catch((err) => {
        console.error(
          `[NetworkInterceptor] auto-reinject failed: ${err instanceof Error ? err.message : String(err)}`,
        );
      });
    });
    return typeof unsubscribe === 'function' ? unsubscribe : () => undefined;
  }

  private async inject(client: InterceptorClient): Promise<void> {
    const rulesJson = JSON.stringify(this.listRules());
    const offlineFlag = this._offline ? 'true' : 'false';
    const script = '(function(){if(!window.__osOriginalFetch){window.__osOriginalFetch=window.fetch.bind(window)}if(!window.__osOriginalXHROpen){window.__osOriginalXHROpen=XMLHttpRequest.prototype.open}if(!window.__osOriginalXHRSend){window.__osOriginalXHRSend=XMLHttpRequest.prototype.send}window.__osInterceptRules=' + rulesJson + ';window.__osOfflineMode=' + offlineFlag + ';function m(u,p){if(p.indexOf("*")===-1&&p.indexOf("?")===-1)return u.indexOf(p)!==-1;var r="";for(var i=0;i<p.length;i++){var c=p[i];if(c==="*"&&p[i+1]==="*"){r+=".*";i++;if(p[i+1]==="/")i++}else if(c==="*")r+="[^/]*";else if(c==="?")r+="[^/]";else if(c===".")r+="\\\\.";else if("^$+{}[]|()\\\\".indexOf(c)!==-1)r+="\\\\"+c;else r+=c}return new RegExp("^"+r+"$").test(u)}function f(u){var rules=window.__osInterceptRules||[];for(var i=0;i<rules.length;i++)if(m(u,rules[i].urlPattern))return rules[i];return null}window.fetch=function(input,init){var url=(typeof input==="string")?input:(input&&input.url?input.url:"");if(window.__osOfflineMode)return Promise.reject(new TypeError("Failed to fetch"));var rule=f(url);if(rule){if(rule.action==="block")return Promise.reject(new TypeError("Request blocked by OpenSafari"));if(rule.action==="mock"&&rule.mockResponse)return Promise.resolve(new Response(rule.mockResponse.body||"",{status:rule.mockResponse.status||200,headers:new Headers(rule.mockResponse.headers||{})}))}return window.__osOriginalFetch(input,init)};XMLHttpRequest.prototype.open=function(method,url){this.__osUrl=url;this.__osBlocked=false;this.__osMatchedRule=null;if(window.__osOfflineMode){this.__osBlocked=true}else{var rule=f(String(url));if(rule){this.__osMatchedRule=rule;if(rule.action==="block")this.__osBlocked=true}}return window.__osOriginalXHROpen.apply(this,arguments)};XMLHttpRequest.prototype.send=function(body){if(this.__osBlocked){Object.defineProperty(this,"status",{get:function(){return 0}});Object.defineProperty(this,"readyState",{get:function(){return 4}});if(typeof this.onerror==="function")this.onerror(new Error("Blocked"));this.dispatchEvent(new Event("error"));return}if(this.__osMatchedRule&&this.__osMatchedRule.action==="mock"){var mk=this.__osMatchedRule.mockResponse||{};var s=this;Object.defineProperty(s,"status",{get:function(){return mk.status||200}});Object.defineProperty(s,"responseText",{get:function(){return mk.body||""}});Object.defineProperty(s,"readyState",{get:function(){return 4}});setTimeout(function(){if(typeof s.onload==="function")s.onload(new Event("load"));s.dispatchEvent(new Event("load"))},0);return}return window.__osOriginalXHRSend.apply(this,arguments)}})()';
    await client.evaluate(script);
  }
}
