import { Provide, Inject, Logger, ILogger } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel, InjectDataSource } from '@midwayjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import axios from 'axios';
import { GeoAccountEntity } from '../entity/account.js';
import { GeoProxyIpEntity } from '../entity/proxy_ip.js';
import { GeoProxyIpService } from './proxy_ip.js';

export interface AddAccountDto {
  name: string;            // 昵称
  platform: string;        // 平台
  proxyId?: number;        // 第三方 IP 模式时填，本地模式不填
  agentId?: number;        // 关联的 Agent ID
  sessionName?: string;    // 不填后端自动生成 `${platform}-${id}`
  loginAccount?: string;   // 一般留空，登录后回填
}

const NB_DAEMON_BASE = process.env.NB_DAEMON_BASE || 'http://127.0.0.1:8003';

@Provide()
export class GeoAccountService extends BaseService {
  @InjectEntityModel(GeoAccountEntity)
  accountEntity: Repository<GeoAccountEntity>;

  @InjectEntityModel(GeoProxyIpEntity)
  proxyIpEntity: Repository<GeoProxyIpEntity>;

  @InjectDataSource()
  dataSource: DataSource;

  @Inject() proxyService: GeoProxyIpService;

  @Logger() logger: ILogger;

  /**
   * 新增账号：
   * - IP 模式：不传 proxyId = 本地直连；传 proxyId = 第三方独立 IP（1:1 绑定）
   * - 用户选择 agent（可选，由该 agent 执行后续操作）
   * - sessionName 自动生成 `${platform}-${id}`，account 通过它跟 netabrowser-cli 的 profile 绑定
   * - loginAccount 通常留空，等用户在 launch 出来的浏览器里登录后从 cookie 自动获取
   */
  async add(dto: AddAccountDto): Promise<GeoAccountEntity> {
    // 1. 校验 IP（仅第三方模式）
    if (dto.proxyId) {
      const ip = await this.proxyIpEntity.findOneBy({ id: dto.proxyId });
      if (!ip) throw new Error(`IP ${dto.proxyId} 不存在`);
      if (ip.bindAccountId) throw new Error(`IP ${dto.proxyId} 已被其他账号绑定`);
      if (ip.status === 'deleted') throw new Error(`IP ${dto.proxyId} 已删除`);
    }

    // 2. 事务：创建 account + 反向绑定 IP（如果有）
    return this.dataSource.transaction(async manager => {
      const repo = manager.getRepository(GeoAccountEntity);
      let account = await repo.save(repo.create({
        name: dto.name,
        platform: dto.platform,
        loginAccount: dto.loginAccount || null,
        loginStatus: 'never',
        proxyId: dto.proxyId || null,
        agentId: dto.agentId || null,
      }));
      // 自动生成 sessionName + fingerprintSeed（每个账号独立指纹，不同账号 seed 不同 → 看作不同物理设备）
      // sessionName 带时间戳后缀，避免复用同名旧 profile（fingerprint-chromium 复用旧 profile 时 --fingerprint 不生效）
      const sessionName = dto.sessionName || `${dto.platform}-${account.id}-${Date.now().toString(36)}`;
      const fingerprintSeed = this.genFingerprintSeed();
      await repo.update(account.id, { sessionName, fingerprintSeed });
      account.sessionName = sessionName;
      account.fingerprintSeed = fingerprintSeed;
      // 绑定 IP（仅第三方模式）
      if (dto.proxyId) {
        await manager.update(GeoProxyIpEntity, dto.proxyId, { bindAccountId: account.id, status: 'active' });
      }
      return account;
    });
  }

  /** 生成随机 fingerprintSeed（1-2147483647，避开 0） */
  private genFingerprintSeed(): number {
    return Math.floor(Math.random() * 2147483646) + 1;
  }

  /**
   * 启动浏览器（通过 netabrowser-cli daemon）
   * - 用 account.sessionName 作为浏览器 session 标识
   * - 把账号绑定的 IP 作为 SOCKS5/HTTP 代理传给 daemon
   */
  async launch(id: number, url?: string): Promise<{ sessionName: string; url: string }> {
    const acc = await this.accountEntity.findOneBy({ id });
    if (!acc) throw new Error(`account ${id} not found`);
    if (!acc.sessionName) throw new Error(`account ${id} sessionName 缺失`);

    // 拿绑定的 IP
    let proxy: any;
    if (acc.proxyId) {
      const ip = await this.proxyIpEntity.findOneBy({ id: acc.proxyId });
      if (ip) {
        const info = this.proxyService.toProxyInfo(ip);
        proxy = {
          server: `${info.protocol}://${info.host}:${info.port}`,
          username: info.username,
          password: info.password,
        };
      }
    }

    // 老账号没生成过 seed 的兜底（迁移用）
    let fingerprintSeed = acc.fingerprintSeed;
    if (!fingerprintSeed) {
      fingerprintSeed = this.genFingerprintSeed();
      await this.accountEntity.update(id, { fingerprintSeed });
    }

    // 检测 profile 是否是 fresh（首次启动，没有 Cookies 文件）
    const profileDir = await this.getProfileDir(acc.sessionName);
    const isFreshProfile = !await this.profileHasCookies(profileDir);

    const targetUrl = url || this.guessLaunchUrl(acc.platform, acc.loginStatus);
    // 尝试 open → 409 已存在则 goto 复用 → goto 也失败（浏览器被关了）则 close + 重新 open
    let opened = false;
    for (let attempt = 0; attempt < 2 && !opened; attempt++) {
      try {
        const r = await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/open`, {
          sessionName: acc.sessionName,
          url: targetUrl,
          headed: true,
          proxy,
          fingerprintSeed,
        }, { timeout: 90_000 });
        if (r.data?.code === 1000) { opened = true; break; }
        throw new Error(r.data?.error || 'open failed');
      } catch (e: any) {
        const is409 = e?.response?.status === 409
          || /already exists/i.test(e?.response?.data?.error || e?.message || '');
        if (!is409) throw e;
        // session 已存在 → 尝试 goto 复用
        try {
          const g = await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/goto`, {
            sessionName: acc.sessionName, url: targetUrl,
          }, { timeout: 60_000 });
          if (g.data?.code === 1000) { opened = true; break; }
          throw new Error(g.data?.error || 'goto failed');
        } catch (gotoErr: any) {
          // goto 也失败（浏览器窗口被关了）→ close 清理后下一轮 open
          this.logger?.warn?.(`[GEO] session ${acc.sessionName} goto failed, closing and retrying: ${gotoErr.message}`);
          await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/close`, {
            sessionName: acc.sessionName,
          }, { timeout: 15_000 }).catch(() => undefined);
        }
      }
    }
    if (!opened) throw new Error(`无法启动浏览器 session ${acc.sessionName}`);

    // Fresh profile + 数据库有 cookies → 注入数据库 cookies 避免重新登录
    if (isFreshProfile && acc.cookies) {
      try {
        const cookies = JSON.parse(acc.cookies);
        if (Array.isArray(cookies) && cookies.length > 0) {
          // 写临时文件给 daemon state-load 用
          const fs = await import('node:fs/promises');
          const path = await import('node:path');
          const os = await import('node:os');
          const tmpFile = path.join(os.tmpdir(), `geo-state-${id}-${Date.now()}.json`);
          await fs.writeFile(tmpFile, JSON.stringify({ cookies, origins: [] }), 'utf8');
          try {
            await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/state-load`, {
              sessionName: acc.sessionName,
              filePath: tmpFile,
            }, { timeout: 30_000 });
            this.logger?.info?.(`[GEO] account ${id} 注入 ${cookies.length} 条 cookie 到新 profile`);
            // 注入 cookie 后 reload 页面让 cookie 生效
            await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/goto`, {
              sessionName: acc.sessionName,
              url: this.guessHomeUrl(acc.platform),
            }, { timeout: 30_000 }).catch(() => undefined);
          } finally {
            await fs.unlink(tmpFile).catch(() => undefined);
          }
        }
      } catch (e: any) {
        this.logger?.warn?.(`[GEO] account ${id} cookie 注入失败: ${e.message}`);
      }
    }

    return { sessionName: acc.sessionName, url: targetUrl };
  }

  /** 拿账号 profile 目录的绝对路径 */
  private async getProfileDir(sessionName: string): Promise<string> {
    const path = await import('node:path');
    return path.resolve(process.cwd(), '..', '..', '.netabrowser-data', 'profiles', sessionName);
  }

  /** 判断 profile 目录是否有 Cookies 文件（首次启动后才会创建） */
  private async profileHasCookies(profileDir: string): Promise<boolean> {
    const fs = await import('node:fs/promises');
    const path = await import('node:path');
    // chromium 把 cookies 存在 Default/Network/Cookies
    const cookieFile = path.join(profileDir, 'Default', 'Network', 'Cookies');
    try {
      const stat = await fs.stat(cookieFile);
      return stat.size > 0;
    } catch {
      return false;
    }
  }

  /** 根据登录状态决定打开哪个 URL：已登录→首页，未登录→登录页 */
  private guessLaunchUrl(platform: string, loginStatus: string): string {
    if (loginStatus === 'logged_in') return this.guessHomeUrl(platform);
    return this.guessLoginUrl(platform);
  }

  /** 平台首页（已登录时用） */
  private guessHomeUrl(platform: string): string {
    const map: Record<string, string> = {
      taobao: 'https://www.taobao.com',
      xiaohongshu: 'https://www.xiaohongshu.com',
      douyin: 'https://www.douyin.com',
      weibo: 'https://weibo.com',
      zhihu: 'https://www.zhihu.com',
      wechat: 'https://wx.qq.com',
    };
    return map[platform] || 'https://www.baidu.com';
  }

  /** 平台登录页（未登录时用） */
  private guessLoginUrl(platform: string): string {
    const map: Record<string, string> = {
      taobao: 'https://login.taobao.com/havanaone/login/login.htm',
      xiaohongshu: 'https://www.xiaohongshu.com',
      douyin: 'https://www.douyin.com',
      weibo: 'https://passport.weibo.com/sso/signin',
      zhihu: 'https://www.zhihu.com/signin',
      wechat: 'https://wx.qq.com',
    };
    return map[platform] || 'https://www.baidu.com';
  }

  /**
   * 抓取 cookie：通过 netabrowser-cli daemon 的 cookie-list 接口
   * 抓完直接明文写库（本地后台工具，无需加密）
   * 同时尝试从 cookie/页面 DOM 中提取登录账号（手机号/用户名），更新 status=active
   */
  async captureCookies(id: number, domain?: string): Promise<{ captured: number; loginAccount?: string }> {
    const acc = await this.accountEntity.findOneBy({ id });
    if (!acc) throw new Error(`account ${id} not found`);
    if (!acc.sessionName) throw new Error(`account ${id} sessionName 缺失`);

    const r = await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/cookie-list`, {
      sessionName: acc.sessionName,
      domain,
    }, { timeout: 30_000 });
    if (r.data?.code !== 1000) throw new Error(r.data?.error || 'cookie-list failed');
    const cookies: any[] = r.data.data || [];
    if (!cookies.length) return { captured: 0 };

    // 尝试从 cookie + DOM 提取登录账号（手机号/用户名/uid）
    const loginAccount = await this.extractLoginAccount(acc.platform, acc.sessionName, cookies);
    // 计算最早过期时间（剔除 session cookie 即 expires < 0）
    const cookieExpiresAt = this.computeEarliestExpiry(cookies);

    await this.accountEntity.update(id, {
      cookies: JSON.stringify(cookies),
      cookieCapturedAt: new Date(),
      cookieExpiresAt,
      loginStatus: 'logged_in',
      ...(loginAccount ? { loginAccount } : {}),
      lastActiveAt: new Date(),
    });
    return { captured: cookies.length, loginAccount };
  }

  /** 取所有非 session cookie 中最早的过期时间（即最先失效的关键登录 cookie） */
  private computeEarliestExpiry(cookies: any[]): Date | null {
    let earliest: number | null = null;
    for (const c of cookies) {
      // playwright cookie expires 是 unix 秒（-1 表示 session cookie）
      const e = typeof c.expires === 'number' ? c.expires : -1;
      if (e <= 0) continue;
      if (earliest === null || e < earliest) earliest = e;
    }
    return earliest ? new Date(earliest * 1000) : null;
  }

  /**
   * 尝试从 cookies 和页面 JS 里提取登录账号（手机号/用户名/uid）
   * 各平台 cookie 字段不同，按平台分别处理；失败时返回 undefined
   */
  private async extractLoginAccount(platform: string, sessionName: string, cookies: any[]): Promise<string | undefined> {
    const cookieMap = new Map<string, string>();
    for (const c of cookies) cookieMap.set(c.name, c.value);

    // 1. 从 cookie 直接解析（淘宝/拼多多等会把昵称/uid 写在 cookie 里）
    const cookieField = (platform: string): string | undefined => {
      if (platform === 'taobao') {
        // 淘宝：dnk = URL编码的昵称；tracknick = 昵称；t/_tb_token_ 是 uid 相关
        const dnk = cookieMap.get('dnk') || cookieMap.get('tracknick');
        if (dnk) {
          try { return decodeURIComponent(dnk).replace(/^"|"$/g, '').replace(/\\u([0-9a-f]{4})/gi, (_, h) => String.fromCharCode(parseInt(h, 16))); }
          catch { return dnk; }
        }
      }
      if (platform === 'xiaohongshu') {
        const u = cookieMap.get('webBuild') || cookieMap.get('userId');
        if (u) return u;
      }
      if (platform === 'douyin') {
        const u = cookieMap.get('passport_csrf_token') || cookieMap.get('sessionid');
        if (u) return u.slice(0, 16) + '...';  // 太长截断
      }
      return undefined;
    };
    const fromCookie = cookieField(platform);
    if (fromCookie) return fromCookie;

    // 2. 从页面 DOM 拿（兜底）—— 通过 daemon eval 执行 JS
    try {
      const r = await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/eval`, {
        sessionName,
        script: `(() => {
          const sels = [
            '.site-nav-login-info-nick', '.user-nick', '[class*="user-name"]', '[class*="userName"]',
            '[data-user-nick]', '.username', '.nick-name'
          ];
          for (const s of sels) {
            const el = document.querySelector(s);
            if (el && el.textContent && el.textContent.trim().length > 0 && el.textContent.trim().length < 32) {
              return el.textContent.trim();
            }
          }
          return null;
        })()`,
      }, { timeout: 10_000 });
      const v = r.data?.data?.result;
      if (typeof v === 'string' && v.length > 0) return v;
    } catch { /* 兜底失败就放弃 */ }

    return undefined;
  }

  /**
   * 重置账号会话：关浏览器 + 清 cookie + 重置 sessionName + 重置 fingerprintSeed + 删旧 profile 目录
   * 用于「无历史负担重新登录」场景。不动 IP 绑定。
   */
  async resetSession(accountId: number): Promise<void> {
    const acc = await this.accountEntity.findOneByOrFail({ id: accountId });
    const oldSessionName = acc.sessionName;

    // 1. 关闭可能正在跑的浏览器
    if (oldSessionName) {
      await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/close`, {
        sessionName: oldSessionName,
      }, { timeout: 15_000 }).catch(() => undefined);
    }

    // 2. 重置 sessionName + fingerprintSeed + 清 cookie 状态
    const newSessionName = `${acc.platform}-${accountId}-${Date.now().toString(36)}`;
    await this.accountEntity.update(accountId, {
      sessionName: newSessionName,
      fingerprintSeed: this.genFingerprintSeed(),
      cookies: null,
      cookieCapturedAt: null,
      cookieExpiresAt: null,
      loginAccount: null,
      loginStatus: 'never',
      lastActiveAt: null,
    });

    // 3. 删除旧 profile 目录（释放磁盘）
    if (oldSessionName) {
      try {
        const fs = await import('node:fs/promises');
        const path = await import('node:path');
        const profileDir = path.resolve(process.cwd(), '..', '..', '.netabrowser-data', 'profiles', oldSessionName);
        await fs.rm(profileDir, { recursive: true, force: true });
        this.logger?.info?.(`[GEO] account ${accountId} reset: 旧 profile ${oldSessionName} 已删除`);
      } catch (e: any) {
        this.logger?.warn?.(`[GEO] account ${accountId} 删除旧 profile 失败: ${e.message}`);
      }
    }
  }

  /** 关闭浏览器 */
  async close(id: number): Promise<void> {
    const acc = await this.accountEntity.findOneBy({ id });
    if (!acc?.sessionName) return;
    await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/close`, {
      sessionName: acc.sessionName,
    }, { timeout: 15_000 }).catch(() => undefined);
  }

  /** 换绑 IP（沿用 setProxy 的重置逻辑：换 IP = 全新 sessionName + 清 cookie） */
  async rebindIp(accountId: number, newIpId: number): Promise<void> {
    return this.setProxy(accountId, newIpId);
  }

  /**
   * 设置账号的 IP 模式（本地 / 第三方双向切换）
   * - newProxyId === null/undefined → 切到本地（解绑现有 IP）
   * - newProxyId === number → 切到第三方（绑定该 IP，自动解绑旧的）
   *
   * 关键养号约束：IP 变更时必须重置 sessionName + cookies + 关闭浏览器，
   * 否则风控会发现"同一 fingerprint+cookie，IP 突然变了" → 直接判异常。
   * 重置后用户需要重新登录。
   */
  async setProxy(accountId: number, newProxyId: number | null): Promise<void> {
    const acc = await this.accountEntity.findOneByOrFail({ id: accountId });
    // 同 IP 直接返回
    if ((acc.proxyId ?? null) === (newProxyId ?? null)) return;

    // 1. 关闭可能正在跑的浏览器（带旧 IP 的 session）
    if (acc.sessionName) {
      await axios.post(`${NB_DAEMON_BASE}/admin/browser-daemon/close`, {
        sessionName: acc.sessionName,
      }, { timeout: 15_000 }).catch(() => undefined);
    }

    await this.dataSource.transaction(async manager => {
      // 2. 解绑旧 IP（如果有）
      if (acc.proxyId) {
        await manager.update(GeoProxyIpEntity, acc.proxyId, { bindAccountId: null, status: 'unbound' });
      }

      if (newProxyId == null) {
        // 切到本地
        await manager.update(GeoAccountEntity, accountId, { proxyId: null });
      } else {
        // 切到第三方
        const newIp = await manager.findOne(GeoProxyIpEntity, { where: { id: newProxyId } });
        if (!newIp) throw new Error(`IP ${newProxyId} not found`);
        if (newIp.bindAccountId && newIp.bindAccountId !== accountId) {
          throw new Error(`IP ${newProxyId} 已绑定其他账号`);
        }
        await manager.update(GeoProxyIpEntity, newProxyId, { bindAccountId: accountId, status: 'active' });
        await manager.update(GeoAccountEntity, accountId, { proxyId: newProxyId });
      }

      // 3. 重置 sessionName + fingerprintSeed（生成新的，让 netabrowser-cli 用全新 profile 目录、全新指纹）
      // 4. 清空 cookies + 重置登录状态
      const newSessionName = `${acc.platform}-${accountId}-${Date.now().toString(36)}`;
      const newFingerprintSeed = this.genFingerprintSeed();
      await manager.update(GeoAccountEntity, accountId, {
        sessionName: newSessionName,
        fingerprintSeed: newFingerprintSeed,
        cookies: null,
        cookieCapturedAt: null,
        cookieExpiresAt: null,
        loginAccount: null,
        loginStatus: 'never',
        lastActiveAt: null,
      });
    });

    // 5. 删除旧 profile 目录（避免磁盘累积，下次 launch 用新 sessionName 创建全新目录）
    if (acc.sessionName) {
      try {
        const fs = await import('node:fs/promises');
        const path = await import('node:path');
        const profileDir = path.join(process.cwd(), '..', '..', '.netabrowser-data', 'profiles', acc.sessionName);
        await fs.rm(profileDir, { recursive: true, force: true }).catch(() => undefined);
        this.logger?.info?.(`[GEO] cleaned old profile dir: ${profileDir}`);
      } catch (e: any) {
        this.logger?.warn?.(`[GEO] clean old profile dir failed: ${e.message}`);
      }
    }
  }

  /** 删除账号（软删除 + 释放 IP 回池；profile 目录在 .netabrowser-data/profiles/<sessionName> 保留） */
  async deleteAccount(id: number): Promise<void> {
    await this.dataSource.transaction(async manager => {
      const acc = await manager.findOneOrFail(GeoAccountEntity, { where: { id } });
      if (acc.proxyId) {
        await manager.update(GeoProxyIpEntity, acc.proxyId, { bindAccountId: null, status: 'unbound' });
      }
      await manager.update(GeoAccountEntity, id, { loginStatus: 'deleted', proxyId: null });
    });
  }
}
