import { Provide, Scope, ScopeEnum, Inject, Logger, ILogger, Init, Destroy } from '@midwayjs/core';
import * as fs from 'node:fs';
import { chromium } from 'patchright';
import { SessionRegistry } from '../runtime/session-registry.js';
import { SessionScheduler, Priority } from '../runtime/session-scheduler.js';
import { FingerprintService } from './fingerprint.service.js';
import { HumanizerService, HumanizeMode } from './humanizer.service.js';
import { SnapshotRefService } from './snapshot-ref.service.js';
import { resolveChromiumPath, buildLaunchArgs } from '../runtime/chromium-launcher.js';
import { getProfileDir, getStateDir } from '../runtime/browser-data-dir.js';
import { gracefullyClose } from '../runtime/cleanup.js';
import { createSocks5HttpBridge, Socks5Bridge } from '../runtime/socks5-http-bridge.js';

export interface OpenOpts {
  sessionName: string;
  url: string;
  fingerprintSeed?: number;
  proxy?: { server: string; username?: string; password?: string };
  profileDir?: string;
  headed?: boolean;
  priority?: Priority;
  queue?: boolean;
}

export interface SessionInfo {
  sessionName: string;
  url: string;
  pageCount: number;
  status: 'active' | 'idle';
  lastUsedAt: Date;
}

@Provide()
@Scope(ScopeEnum.Singleton)
export class BrowserDaemonService {
  @Inject() registry: SessionRegistry;
  @Inject() scheduler: SessionScheduler;
  @Inject() fingerprint: FingerprintService;
  @Inject() humanizer: HumanizerService;
  @Inject() snapshotRef: SnapshotRefService;
  @Logger() logger: ILogger;

  /** 允许测试替换 chromium 模块 */
  protected chromiumModule: any = chromium;
  /** 允许测试替换路径解析 */
  protected resolveChromiumPathFn = resolveChromiumPath;

  @Init()
  async init(): Promise<void> {
    // 注册 idle 超时回调：session 空闲超时后自动保存状态并关闭
    this.scheduler.setIdleTimeoutHandler(async (sessionName) => {
      this.logger?.info?.(`[browser-daemon] idle timeout: ${sessionName} → save+close`);
      await this.registry.withLock(sessionName, async () => {
        const ctx = this.registry.get(sessionName);
        if (!ctx) return;
        const stateFile = getStateDir(sessionName);
        try {
          const fsSync = await import('node:fs');
          fsSync.mkdirSync(require('node:path').dirname(stateFile), { recursive: true });
          await ctx.storageState({ path: stateFile });
        } catch (e: any) {
          this.logger?.warn?.(`[browser-daemon] saveState on idle failed: ${e.message}`);
        }
        await this.closeInternal(sessionName);
      });
    });

    // 统计磁盘上已有的 session 状态文件
    const fsSync = await import('node:fs');
    const pathMod = await import('node:path');
    const stateRoot = pathMod.dirname(getStateDir('placeholder'));
    if (fsSync.existsSync(stateRoot)) {
      const files = fsSync.readdirSync(stateRoot).filter((f: string) => f.endsWith('.json'));
      this.logger?.info?.(`[browser-daemon] init: ${files.length} known sessions on disk`);
    }
  }

  @Destroy()
  async destroy(): Promise<void> {
    this.logger?.info?.('[browser-daemon] destroying, closing all sessions...');
    const all = this.registry.list();
    await Promise.all(all.map(async (e) => {
      try {
        const stateFile = getStateDir(e.sessionName);
        const fsSync = await import('node:fs');
        fsSync.mkdirSync(require('node:path').dirname(stateFile), { recursive: true });
        await e.context.storageState({ path: stateFile });
      } catch (err: any) {
        this.logger?.warn?.(`[browser-daemon] saveState ${e.sessionName} failed: ${err.message}`);
      }
      await gracefullyClose(e.context, 5000);
    }));
  }

  async open(opts: OpenOpts): Promise<{ sessionName: string; url: string; pageCount: number }> {
    return this.registry.withLock(opts.sessionName, async () => {
      if (this.registry.has(opts.sessionName)) {
        const err: any = new Error(`Session '${opts.sessionName}' already exists`);
        err.statusCode = 409;
        throw err;
      }

      const slot = await this.scheduler.acquireSlot(
        opts.sessionName,
        opts.priority ?? 'normal',
        opts.queue ?? false,
      );
      if (!slot.granted) {
        const err: any = new Error(`No slot available: ${slot.reason}`);
        err.statusCode = 503;
        err.retryAfter = 30;
        throw err;
      }
      if (slot.evictedSessionName) {
        await this.closeInternal(slot.evictedSessionName);
      }

      const profilePath = getProfileDir(opts.profileDir ?? opts.sessionName);
      const isFreshProfile = !fs.existsSync(profilePath) || fs.readdirSync(profilePath).length === 0;
      fs.mkdirSync(profilePath, { recursive: true });

      // 不传 fingerprintSeed 时完全不传 fingerprint-* 参数，避免 fingerprint-chromium
      // 修改字体列表导致部分中文字体匹配失败（左侧菜单乱码问题）
      const fingerprintArgs = opts.fingerprintSeed != null
        ? this.fingerprint.fromSeed(opts.fingerprintSeed)
        : [];
      const args = buildLaunchArgs({ fingerprintArgs });

      // chromium 不支持 SOCKS5 用户名/密码认证，自动起本地 HTTP→SOCKS5 桥
      let proxyForChromium = opts.proxy;
      let socks5Bridge: Socks5Bridge | undefined;
      if (opts.proxy?.server && /^socks5:\/\//i.test(opts.proxy.server) && (opts.proxy.username || opts.proxy.password)) {
        const u = new URL(opts.proxy.server);
        socks5Bridge = await createSocks5HttpBridge({
          host: u.hostname,
          port: parseInt(u.port, 10),
          username: opts.proxy.username,
          password: opts.proxy.password,
        });
        proxyForChromium = { server: socks5Bridge.localUrl };
        this.logger?.info?.(`[browser-daemon] SOCKS5 auth detected, bridging via ${socks5Bridge.localUrl}`);
      }

      this.logger?.info?.(`[browser-daemon] launching ${opts.sessionName} → ${opts.url} proxy=${proxyForChromium?.server ?? 'none'}`);
      let ctx: any;
      try {
        ctx = await this.chromiumModule.launchPersistentContext(profilePath, {
          executablePath: this.resolveChromiumPathFn(),
          headless: !opts.headed,
          args,
          proxy: proxyForChromium,
          locale: 'zh-CN',
          ignoreDefaultArgs: ['--enable-automation'],
        });
      } catch (e: any) {
        socks5Bridge?.close();
        this.logger?.error?.(`[browser-daemon] launchPersistentContext failed: ${e.message}`);
        throw e;
      }
      // 把桥挂到 ctx 上，session close 时一起关
      if (socks5Bridge) (ctx as any).__netaSocks5Bridge = socks5Bridge;

      const page = ctx.pages()[0] ?? (await ctx.newPage());
      await this.injectCursorOverlay(page);
      // 显式设置中文 script 的字体 fallback：
      // 解决 portable chromium 在某些 locale 检测下，左侧菜单等 CSS 指定字体 fallback 到 Latin 字体导致中文乱码
      await this.setChineseFontFamilies(ctx, page);
      // 用 domcontentloaded 而不是默认的 'load'：
      // - 'load' 等所有资源（图片/CSS/脚本）→ 走代理时常超 30s
      // - 'domcontentloaded' DOM 解析完即返回 → 通常 5-15s
      // 走代理 + headed 时给 25s 超时（比 NetaClaw bash 默认 30s 短，留余量返回）
      try {
        await page.goto(opts.url, { waitUntil: 'domcontentloaded', timeout: 25000 });
      } catch (e: any) {
        // navigation 失败不要崩溃 open，让 AI 拿到 session 后用 wait/snapshot 自己排查
        this.logger?.warn?.(`[browser-daemon] goto ${opts.url} failed: ${e.message}`);
      }
      // 异步资源继续加载（不阻塞 open 返回）
      try {
        await page.waitForLoadState('networkidle', { timeout: 5000 });
      } catch {}
      // 首次创建 profile 时字体后台预热可能慢导致部分中文乱码，强制 reload 一次让字体重新匹配
      if (isFreshProfile) {
        try {
          await page.reload({ waitUntil: 'domcontentloaded' });
          await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
        } catch {}
      }
      // 激活 chromium 的鼠标 hover 状态：第一次 CDP mouse 事件如果光标在视口外会被吞掉
      // 移动到视口中央偏移位让后续 humanizer move 能正常派发 mousemove DOM 事件
      try {
        const vp = page.viewportSize?.() ?? { width: 1280, height: 720 };
        await page.mouse.move(Math.floor(vp.width / 2), Math.floor(vp.height / 2));
      } catch {}

      this.registry.register(opts.sessionName, ctx);
      this.scheduler.recordActivate(opts.sessionName, opts.priority ?? 'normal');

      return { sessionName: opts.sessionName, url: page.url(), pageCount: ctx.pages().length };
    });
  }

  /** 关闭指定 session（带锁） */
  async close(sessionName: string): Promise<void> {
    return this.registry.withLock(sessionName, () => this.closeInternal(sessionName));
  }

  /** 内部关闭逻辑（不带锁，供 open 驱逐和后续 cleanup 调用） */
  async closeInternal(sessionName: string): Promise<void> {
    const ctx = this.registry.get(sessionName);
    if (!ctx) return;
    try {
      await ctx.close();
    } catch (e: any) {
      this.logger?.warn?.(`[browser-daemon] close ${sessionName} failed: ${e.message}`);
    }
    // 关闭与 session 绑定的 SOCKS5 桥
    const bridge = (ctx as any).__netaSocks5Bridge as Socks5Bridge | undefined;
    if (bridge) {
      try { bridge.close(); } catch {}
    }
    this.registry.unregister(sessionName);
    this.scheduler.recordDeactivate(sessionName);
  }

  /** 列出所有活跃 session */
  list(): SessionInfo[] {
    return this.registry.list().map(e => {
      const ctx = e.context;
      const page = ctx.pages?.()[0];
      return {
        sessionName: e.sessionName,
        url: page?.url?.() ?? '',
        pageCount: ctx.pages?.().length ?? 0,
        status: 'active' as const,
        lastUsedAt: e.lastUsedAt,
      };
    });
  }

  /** 返回调度器统计信息 */
  getStats() {
    return this.scheduler.getStats();
  }

  /** 获取 session 的第一个 page，不存在则抛错（含自动清理 stale session 提示） */
  getPageOrFail(sessionName: string) {
    const ctx = this.registry.get(sessionName);
    if (!ctx) throw new Error(`Session '${sessionName}' not found`);
    const page = ctx.pages()[0];
    if (!page) {
      // 用户手动关了浏览器窗口，session 还在 registry 但没有 page → 自动清理
      this.registry.unregister(sessionName);
      this.scheduler.recordDeactivate(sessionName);
      throw new Error(`Session '${sessionName}' has no page (browser window was closed). Session has been cleaned up — use 'open' to restart.`);
    }
    this.registry.touch(sessionName);
    this.scheduler.touch(sessionName);
    return { ctx, page };
  }

  /** 公共 actionability 检查：waitFor visible + scrollIntoView + 拿 boundingBox 中心点 */
  protected async resolveActionablePoint(page: any, ref: string): Promise<{ locator: any; cx: number; cy: number }> {
    const locator = page.locator(this.snapshotRef.refToSelector(ref));
    await locator.waitFor({ state: 'visible', timeout: 5000 });
    await locator.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
    const box = await locator.boundingBox();
    if (!box) throw new Error(`Ref '${ref}' has no bounding box (display:none/视口外)`);
    return { locator, cx: box.x + box.width / 2, cy: box.y + box.height / 2 };
  }

  async click(sessionName: string, ref: string, mode: HumanizeMode = 'full'): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { ctx, page } = this.getPageOrFail(sessionName);
      const { cx, cy } = await this.resolveActionablePoint(page, ref);
      await this.humanizer.click(ctx, page, { x: cx, y: cy }, mode);
    });
  }

  async hover(sessionName: string, ref: string, mode: HumanizeMode = 'full'): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { ctx, page } = this.getPageOrFail(sessionName);
      const { cx, cy } = await this.resolveActionablePoint(page, ref);
      await this.humanizer.hover(ctx, page, { x: cx, y: cy }, mode);
    });
  }

  async fill(sessionName: string, ref: string, value: string, mode: HumanizeMode = 'full'): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { ctx, page } = this.getPageOrFail(sessionName);
      const { locator, cx, cy } = await this.resolveActionablePoint(page, ref);
      // 拟人化移动到目标 + clear + 拟人化输入
      await this.humanizer.hover(ctx, page, { x: cx, y: cy }, mode);
      await locator.fill(''); // 内置 fill 自动 focus + clear
      await this.humanizer.type(page, value, mode);
    });
  }

  async type(sessionName: string, text: string, mode: HumanizeMode = 'full'): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      await this.humanizer.type(page, text, mode);
    });
  }

  async selectOption(sessionName: string, ref: string, value: string | string[]): Promise<string[]> {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      const locator = page.locator(this.snapshotRef.refToSelector(ref));
      await locator.waitFor({ state: 'visible', timeout: 5000 });
      return await locator.selectOption(value);
    });
  }

  async check(sessionName: string, ref: string, checked: boolean = true): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      const locator = page.locator(this.snapshotRef.refToSelector(ref));
      await locator.waitFor({ state: 'visible', timeout: 5000 });
      if (checked) await locator.check();
      else await locator.uncheck();
    });
  }

  async pressKey(sessionName: string, key: string): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      await page.keyboard.press(key);
    });
  }

  async scroll(sessionName: string, deltaY: number, mode: HumanizeMode = 'full'): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      await this.humanizer.scroll(page, deltaY, mode);
    });
  }

  async waitFor(sessionName: string, opts: { ref?: string; state?: 'visible' | 'hidden' | 'attached' | 'detached'; loadState?: 'load' | 'domcontentloaded' | 'networkidle'; timeout?: number }): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      const timeout = opts.timeout ?? 10000;
      if (opts.ref) {
        const locator = page.locator(this.snapshotRef.refToSelector(opts.ref));
        await locator.waitFor({ state: opts.state ?? 'visible', timeout });
      } else if (opts.loadState) {
        await page.waitForLoadState(opts.loadState, { timeout });
      } else {
        await page.waitForLoadState('networkidle', { timeout });
      }
    });
  }

  async screenshot(sessionName: string, opts: { fullPage?: boolean; quality?: number } = {}): Promise<string> {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      const buf = await page.screenshot({ fullPage: opts.fullPage ?? false, type: 'png' });
      return (buf as Buffer).toString('base64');
    });
  }

  async evalScript(sessionName: string, script: string): Promise<any> {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      return await page.evaluate(script);
    });
  }

  async snapshot(sessionName: string) {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      return await this.snapshotRef.snapshot(sessionName, page);
    });
  }

  async getCookies(sessionName: string, domain?: string): Promise<any[]> {
    return this.registry.withLock(sessionName, async () => {
      const { ctx } = this.getPageOrFail(sessionName);
      const cookies = await ctx.cookies();
      return domain ? cookies.filter((c: any) => c.domain.includes(domain)) : cookies;
    });
  }

  async saveState(sessionName: string, filePath?: string): Promise<string> {
    return this.registry.withLock(sessionName, async () => {
      const { ctx } = this.getPageOrFail(sessionName);
      const target = filePath ?? getStateDir(sessionName);
      const fsSync = await import('node:fs');
      fsSync.mkdirSync(require('node:path').dirname(target), { recursive: true });
      await ctx.storageState({ path: target });
      return target;
    });
  }

  async loadState(sessionName: string, filePath: string): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { ctx, page } = this.getPageOrFail(sessionName);
      const fsP = await import('node:fs/promises');
      const data = JSON.parse(await fsP.readFile(filePath, 'utf8'));
      if (data.cookies) await ctx.addCookies(data.cookies);
      if (data.origins) {
        for (const o of data.origins) {
          await page.goto(o.origin);
          for (const item of o.localStorage ?? []) {
            await page.evaluate(([k, v]: [string, string]) => localStorage.setItem(k, v), [item.name, item.value]);
          }
        }
      }
    });
  }

  async goto(sessionName: string, url: string): Promise<void> {
    return this.registry.withLock(sessionName, async () => {
      const { page } = this.getPageOrFail(sessionName);
      try {
        await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 25000 });
      } catch (e: any) {
        this.logger?.warn?.(`[browser-daemon] goto ${url} failed: ${e.message}`);
      }
      // goto 后重新激活鼠标 hover 状态（页面重载后内部鼠标位置重置）
      try {
        const vp = page.viewportSize?.() ?? { width: 1280, height: 720 };
        await page.mouse.move(Math.floor(vp.width / 2), Math.floor(vp.height / 2));
      } catch {}
    });
  }

  /**
   * 强制设置中文字体 fallback，解决 portable chromium 在某些 locale 下，
   * CSS 指定字体不存在时 fallback 到 Latin 字体而非中文字体导致左侧菜单等乱码。
   *
   * ⚠️ 反风控约束：禁止用 ctx.newCDPSession + cdp.send（即使是 Page domain），
   * 手动 CDP attach 会暴露 debugger 给反爬探测。
   * 这里只用 patchright 内置的 ctx.addInitScript（已经过反检测处理）注入全局 CSS，
   * 用 font-family + !important 强制覆盖原 CSS，效果等同 Page.setFontFamilies。
   */
  protected async setChineseFontFamilies(_ctx: any, page: any): Promise<void> {
    try {
      const cssInjectScript = `
        (() => {
          if (window.__netaFontPatchInstalled) return;
          window.__netaFontPatchInstalled = true;
          const inject = () => {
            if (!document.documentElement) {
              setTimeout(inject, 10);
              return;
            }
            const style = document.createElement('style');
            style.id = '__neta_font_patch';
            style.textContent = \`
              :root, body, * {
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", "微软雅黑", "Noto Sans CJK SC", "Source Han Sans CN", "SimSun", "宋体", "Helvetica Neue", Arial, sans-serif !important;
              }
              :root, body, *:where(input, textarea, button, select) {
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", "微软雅黑", "SimSun", sans-serif !important;
              }
            \`;
            (document.head ?? document.documentElement).appendChild(style);
          };
          if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', inject);
          }
          inject();
        })();
      `;
      await page.context().addInitScript(cssInjectScript);
    } catch (e: any) {
      this.logger?.warn?.(`[browser-daemon] font CSS init script failed: ${e.message}`);
    }
  }

  /**
   * 注入可视化光标 overlay，跟随页面内 mousemove 事件显示一个红色小圆点。
   * 仅用于调试观察拟人化轨迹，不影响反风控（光标 div 设了 pointer-events:none）。
   */
  protected async injectCursorOverlay(page: any): Promise<void> {
    const script = `
      (() => {
        if (window.__netaCursorInstalled) return;
        window.__netaCursorInstalled = true;
        const install = () => {
          if (!document.body) return;
          const dot = document.createElement('div');
          dot.id = '__neta_cursor';
          dot.style.cssText = [
            'position:fixed','top:0','left:0','width:14px','height:14px',
            'border-radius:50%','background:rgba(255,0,0,0.7)',
            'border:2px solid #fff','box-shadow:0 0 6px rgba(255,0,0,0.9)',
            'pointer-events:none','z-index:2147483647','transform:translate(-50%,-50%)',
            'transition:transform 0.05s linear',
          ].join(';');
          document.body.appendChild(dot);
          document.addEventListener('mousemove', (e) => {
            dot.style.left = e.clientX + 'px';
            dot.style.top = e.clientY + 'px';
          }, true);
          document.addEventListener('mousedown', () => {
            dot.style.background = 'rgba(0,200,0,0.9)';
            dot.style.transform = 'translate(-50%,-50%) scale(1.5)';
          }, true);
          document.addEventListener('mouseup', () => {
            dot.style.background = 'rgba(255,0,0,0.7)';
            dot.style.transform = 'translate(-50%,-50%) scale(1)';
          }, true);
        };
        if (document.readyState === 'loading') {
          document.addEventListener('DOMContentLoaded', install);
        } else {
          install();
        }
      })();
    `;
    try {
      await page.context().addInitScript(script);
      await page.evaluate(script);
    } catch (e: any) {
      this.logger?.warn?.(`[browser-daemon] injectCursorOverlay failed: ${e.message}`);
    }
  }
}
