import { createRequire } from 'node:module';
import { randomUUID } from 'node:crypto';
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { deflateSync } from 'node:zlib';
import { normalizeOpenClawAccountId } from './channel-alias';
import { resolveOpenClawRuntimeModulePath } from './runtime-package-resolution';

export const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com';
const DEFAULT_ILINK_BOT_TYPE = '3';
const ACTIVE_LOGIN_TTL_MS = 5 * 60_000;
const QR_POLL_TIMEOUT_MS = 35_000;
const MAX_QR_REFRESH_COUNT = 3;
const OPENCLAW_DIR = join(homedir(), '.openclaw');
const WECHAT_STATE_DIR = join(OPENCLAW_DIR, 'openclaw-weixin');
const WECHAT_ACCOUNT_INDEX_FILE = join(WECHAT_STATE_DIR, 'accounts.json');
const WECHAT_ACCOUNTS_DIR = join(WECHAT_STATE_DIR, 'accounts');
const require = createRequire(import.meta.url);

type QrCodeMatrix = {
  addData(input: string): void;
  make(): void;
  getModuleCount(): number;
  isDark(row: number, col: number): boolean;
};

type QrCodeConstructor = new (typeNumber: number, errorCorrectionLevel: unknown) => QrCodeMatrix;
type QrErrorCorrectLevelModule = {
  L: unknown;
};

type QrRenderDeps = {
  QRCode: QrCodeConstructor;
  QRErrorCorrectLevel: QrErrorCorrectLevelModule;
};

let qrRenderDeps: QrRenderDeps | null = null;

function getQrRenderDeps(): QrRenderDeps {
  if (qrRenderDeps) {
    return qrRenderDeps;
  }

  const qrCodeModulePath = resolveOpenClawRuntimeModulePath('qrcode-terminal/vendor/QRCode/index.js');
  const qrErrorCorrectLevelPath = resolveOpenClawRuntimeModulePath(
    'qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js',
  );
  qrRenderDeps = {
    QRCode: require(qrCodeModulePath),
    QRErrorCorrectLevel: require(qrErrorCorrectLevelPath),
  };
  return qrRenderDeps;
}

type ActiveLogin = {
  sessionKey: string;
  qrcode: string;
  qrcodeUrl: string;
  startedAt: number;
  apiBaseUrl: string;
};

type QrCodeResponse = {
  qrcode: string;
  qrcode_img_content: string;
};

type QrStatusResponse = {
  status: 'wait' | 'scaned' | 'confirmed' | 'expired';
  bot_token?: string;
  ilink_bot_id?: string;
  baseurl?: string;
  ilink_user_id?: string;
};

export type WeChatLoginStartResult = {
  sessionKey: string;
  qrcodeUrl?: string;
  message: string;
};

export type WeChatLoginWaitResult = {
  connected: boolean;
  message: string;
  botToken?: string;
  accountId?: string;
  baseUrl?: string;
  userId?: string;
};

const activeLogins = new Map<string, ActiveLogin>();

function createQrMatrix(input: string) {
  const { QRCode, QRErrorCorrectLevel } = getQrRenderDeps();
  const qr = new QRCode(-1, QRErrorCorrectLevel.L);
  qr.addData(input);
  qr.make();
  return qr;
}

function fillPixel(
  buf: Buffer,
  x: number,
  y: number,
  width: number,
  r: number,
  g: number,
  b: number,
  a = 255,
) {
  const idx = (y * width + x) * 4;
  buf[idx] = r;
  buf[idx + 1] = g;
  buf[idx + 2] = b;
  buf[idx + 3] = a;
}

function crcTable() {
  const table = new Uint32Array(256);
  for (let i = 0; i < 256; i += 1) {
    let c = i;
    for (let k = 0; k < 8; k += 1) {
      c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
    }
    table[i] = c >>> 0;
  }
  return table;
}

const CRC_TABLE = crcTable();

function crc32(buf: Buffer) {
  let crc = 0xffffffff;
  for (let i = 0; i < buf.length; i += 1) {
    crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
  }
  return (crc ^ 0xffffffff) >>> 0;
}

function pngChunk(type: string, data: Buffer) {
  const typeBuf = Buffer.from(type, 'ascii');
  const len = Buffer.alloc(4);
  len.writeUInt32BE(data.length, 0);
  const crc = crc32(Buffer.concat([typeBuf, data]));
  const crcBuf = Buffer.alloc(4);
  crcBuf.writeUInt32BE(crc, 0);
  return Buffer.concat([len, typeBuf, data, crcBuf]);
}

function encodePngRgba(buffer: Buffer, width: number, height: number) {
  const stride = width * 4;
  const raw = Buffer.alloc((stride + 1) * height);
  for (let row = 0; row < height; row += 1) {
    const rawOffset = row * (stride + 1);
    raw[rawOffset] = 0;
    buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride);
  }
  const compressed = deflateSync(raw);
  const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
  const ihdr = Buffer.alloc(13);
  ihdr.writeUInt32BE(width, 0);
  ihdr.writeUInt32BE(height, 4);
  ihdr[8] = 8;
  ihdr[9] = 6;
  ihdr[10] = 0;
  ihdr[11] = 0;
  ihdr[12] = 0;

  return Buffer.concat([
    signature,
    pngChunk('IHDR', ihdr),
    pngChunk('IDAT', compressed),
    pngChunk('IEND', Buffer.alloc(0)),
  ]);
}

async function renderQrPngDataUrl(
  input: string,
  opts: { scale?: number; marginModules?: number } = {},
): Promise<string> {
  const { scale = 6, marginModules = 4 } = opts;
  const qr = createQrMatrix(input);
  const modules = qr.getModuleCount();
  const size = (modules + marginModules * 2) * scale;
  const buf = Buffer.alloc(size * size * 4, 255);

  for (let row = 0; row < modules; row += 1) {
    for (let col = 0; col < modules; col += 1) {
      if (!qr.isDark(row, col)) continue;
      const startX = (col + marginModules) * scale;
      const startY = (row + marginModules) * scale;
      for (let y = 0; y < scale; y += 1) {
        const pixelY = startY + y;
        for (let x = 0; x < scale; x += 1) {
          const pixelX = startX + x;
          fillPixel(buf, pixelX, pixelY, size, 0, 0, 0, 255);
        }
      }
    }
  }

  const png = encodePngRgba(buf, size, size);
  return `data:image/png;base64,${png.toString('base64')}`;
}

function isLoginFresh(login: ActiveLogin): boolean {
  return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
}

function resolveConfigPath(): string {
  const envPath = process.env.OPENCLAW_CONFIG?.trim();
  if (envPath) return envPath;
  return join(OPENCLAW_DIR, 'openclaw.json');
}

function loadWeChatRouteTag(accountId?: string): string | undefined {
  try {
    const configPath = resolveConfigPath();
    if (!existsSync(configPath)) return undefined;
    const raw = readFileSync(configPath, 'utf-8');
    const parsed = JSON.parse(raw) as {
      channels?: Record<string, {
        routeTag?: string | number;
        accounts?: Record<string, { routeTag?: string | number }>;
      }>;
    };
    const section = parsed.channels?.['openclaw-weixin'];
    if (!section) return undefined;
    if (accountId) {
      const normalizedAccountId = normalizeOpenClawAccountId(accountId);
      const scopedRouteTag = section.accounts?.[normalizedAccountId]?.routeTag;
      if (typeof scopedRouteTag === 'number') return String(scopedRouteTag);
      if (typeof scopedRouteTag === 'string' && scopedRouteTag.trim()) return scopedRouteTag.trim();
    }
    if (typeof section.routeTag === 'number') return String(section.routeTag);
    if (typeof section.routeTag === 'string' && section.routeTag.trim()) return section.routeTag.trim();
  } catch {
    return undefined;
  }
  return undefined;
}

async function fetchWeChatQrCode(apiBaseUrl: string, accountId?: string, botType = DEFAULT_ILINK_BOT_TYPE): Promise<QrCodeResponse> {
  const base = apiBaseUrl.endsWith('/') ? apiBaseUrl : `${apiBaseUrl}/`;
  const url = new URL(`ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`, base);
  const headers: Record<string, string> = {};
  const routeTag = loadWeChatRouteTag(accountId);
  if (routeTag) {
    headers.SKRouteTag = routeTag;
  }

  const response = await fetch(url.toString(), { headers });
  if (!response.ok) {
    const body = await response.text().catch(() => '(unreadable)');
    throw new Error(`Failed to fetch QR code: ${response.status} ${response.statusText} ${body}`);
  }
  return await response.json() as QrCodeResponse;
}

async function pollWeChatQrStatus(apiBaseUrl: string, qrcode: string, accountId?: string): Promise<QrStatusResponse> {
  const base = apiBaseUrl.endsWith('/') ? apiBaseUrl : `${apiBaseUrl}/`;
  const url = new URL(`ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`, base);
  const headers: Record<string, string> = {
    'iLink-App-ClientVersion': '1',
  };
  const routeTag = loadWeChatRouteTag(accountId);
  if (routeTag) {
    headers.SKRouteTag = routeTag;
  }

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
  try {
    const response = await fetch(url.toString(), { headers, signal: controller.signal });
    clearTimeout(timer);
    const rawText = await response.text();
    if (!response.ok) {
      throw new Error(`Failed to poll QR status: ${response.status} ${response.statusText} ${rawText}`);
    }
    return JSON.parse(rawText) as QrStatusResponse;
  } catch (error) {
    clearTimeout(timer);
    if (error instanceof Error && error.name === 'AbortError') {
      return { status: 'wait' };
    }
    throw error;
  }
}

async function readAccountIndex(): Promise<string[]> {
  try {
    const raw = await readFile(WECHAT_ACCOUNT_INDEX_FILE, 'utf-8');
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    return parsed.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0);
  } catch {
    return [];
  }
}

async function writeAccountIndex(accountIds: string[]): Promise<void> {
  await mkdir(WECHAT_STATE_DIR, { recursive: true });
  await writeFile(WECHAT_ACCOUNT_INDEX_FILE, JSON.stringify(accountIds, null, 2), 'utf-8');
}

export async function saveWeChatAccountState(rawAccountId: string, payload: {
  token: string;
  baseUrl?: string;
  userId?: string;
}): Promise<string> {
  const accountId = normalizeOpenClawAccountId(rawAccountId);
  await mkdir(WECHAT_ACCOUNTS_DIR, { recursive: true });

  const filePath = join(WECHAT_ACCOUNTS_DIR, `${accountId}.json`);
  const data = {
    token: payload.token.trim(),
    savedAt: new Date().toISOString(),
    ...(payload.baseUrl?.trim() ? { baseUrl: payload.baseUrl.trim() } : {}),
    ...(payload.userId?.trim() ? { userId: payload.userId.trim() } : {}),
  };
  await writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
  try {
    await chmod(filePath, 0o600);
  } catch {
    // best effort only
  }

  const existingAccountIds = await readAccountIndex();
  if (!existingAccountIds.includes(accountId)) {
    await writeAccountIndex([...existingAccountIds, accountId]);
  }

  return accountId;
}

export async function startWeChatLoginSession(options: {
  sessionKey?: string;
  accountId?: string;
  apiBaseUrl?: string;
  force?: boolean;
}): Promise<WeChatLoginStartResult> {
  const sessionKey = options.sessionKey?.trim() || randomUUID();
  const apiBaseUrl = options.apiBaseUrl?.trim() || DEFAULT_WECHAT_BASE_URL;
  const existing = activeLogins.get(sessionKey);

  if (!options.force && existing && isLoginFresh(existing) && existing.qrcodeUrl) {
    return {
      sessionKey,
      qrcodeUrl: existing.qrcodeUrl,
      message: 'QR code is ready. Scan it with WeChat.',
    };
  }

  const qrResponse = await fetchWeChatQrCode(apiBaseUrl, options.accountId);
  const qrDataUrl = await renderQrPngDataUrl(qrResponse.qrcode_img_content);
  activeLogins.set(sessionKey, {
    sessionKey,
    qrcode: qrResponse.qrcode,
    qrcodeUrl: qrDataUrl,
    startedAt: Date.now(),
    apiBaseUrl,
  });

  return {
    sessionKey,
    qrcodeUrl: qrDataUrl,
    message: 'Scan the QR code with WeChat to complete login.',
  };
}

export async function waitForWeChatLoginSession(options: {
  sessionKey: string;
  timeoutMs?: number;
  accountId?: string;
  onQrRefresh?: (payload: { qrcodeUrl: string }) => void | Promise<void>;
}): Promise<WeChatLoginWaitResult> {
  const login = activeLogins.get(options.sessionKey);
  if (!login) {
    return {
      connected: false,
      message: 'No active WeChat login session. Generate a new QR code and try again.',
    };
  }

  if (!isLoginFresh(login)) {
    activeLogins.delete(options.sessionKey);
    return {
      connected: false,
      message: 'The QR code has expired. Generate a new QR code and try again.',
    };
  }

  const timeoutMs = Math.max(options.timeoutMs ?? 480_000, 1000);
  const deadline = Date.now() + timeoutMs;
  let qrRefreshCount = 1;

  while (Date.now() < deadline) {
    const current = activeLogins.get(options.sessionKey);
    if (!current) {
      return {
        connected: false,
        message: 'The WeChat login session was cancelled.',
      };
    }

    const statusResponse = await pollWeChatQrStatus(current.apiBaseUrl, current.qrcode, options.accountId);
    switch (statusResponse.status) {
      case 'wait':
      case 'scaned':
        break;
      case 'expired': {
        qrRefreshCount += 1;
        if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
          activeLogins.delete(options.sessionKey);
          return {
            connected: false,
            message: 'The QR code expired too many times. Generate a new QR code and try again.',
          };
        }
        const refreshedQr = await fetchWeChatQrCode(current.apiBaseUrl, options.accountId);
        const refreshedQrDataUrl = await renderQrPngDataUrl(refreshedQr.qrcode_img_content);
        activeLogins.set(options.sessionKey, {
          ...current,
          qrcode: refreshedQr.qrcode,
          qrcodeUrl: refreshedQrDataUrl,
          startedAt: Date.now(),
        });
        await options.onQrRefresh?.({ qrcodeUrl: refreshedQrDataUrl });
        break;
      }
      case 'confirmed':
        activeLogins.delete(options.sessionKey);
        if (!statusResponse.ilink_bot_id || !statusResponse.bot_token) {
          return {
            connected: false,
            message: 'WeChat login succeeded but the server did not return the required account credentials.',
          };
        }
        return {
          connected: true,
          botToken: statusResponse.bot_token,
          accountId: statusResponse.ilink_bot_id,
          baseUrl: statusResponse.baseurl,
          userId: statusResponse.ilink_user_id,
          message: 'WeChat connected successfully.',
        };
    }

    await new Promise((resolve) => setTimeout(resolve, 1000));
  }

  activeLogins.delete(options.sessionKey);
  return {
    connected: false,
    message: 'Timed out waiting for WeChat QR confirmation.',
  };
}

export async function cancelWeChatLoginSession(sessionKey?: string): Promise<void> {
  if (!sessionKey) {
    activeLogins.clear();
    return;
  }
  activeLogins.delete(sessionKey);
}

export async function clearWeChatLoginState(): Promise<void> {
  activeLogins.clear();
  await rm(WECHAT_STATE_DIR, { recursive: true, force: true });
}
