// Baidu Netdisk Driver v4 — Cookie-based (Playwright QR login + HTTP API)
// Uses full browser Cookie string for all operations (no OAuth access_token needed).
// Share operations use internal web API (/share/verify, /share/transfer, parse HTML).
// Reference: https://github.com/hxz393/BaiduPanFilesTransfers
//
// v4 changes from v3:
//   - QR login via Playwright browser → captures full Cookie string (BDUSS + BAIDUID + STOKEN + ...)
//   - getShareFiles uses Cookie HTTP: getbdstoken → verify password → GET share page → regex parse
//   - transferFiles uses Cookie HTTP: POST /share/transfer
//   - File list/create/delete use Cookie-based /api/* endpoints with bdstoken
//   - Removed OAuth Device Code flow (no more access_token)

import type { Browser, BrowserContext, Page } from 'playwright';

export interface BaiduConfig {
  cookie?: string;            // Full Cookie string: "BDUSS=xxx; BAIDUID=yyy; STOKEN=zzz; ..."
  bdstoken?: string;          // Cached bdstoken from /api/gettemplatevariable
  cookieExpired?: boolean;     // Flag set when cookie validation fails
  nickname?: string;
}

interface ShareFileInfo {
  server_filename: string;
  fs_id: string;
  isdir: number;
  size: number;
  path: string;
  category: number;
}

interface ShareDetail {
  files: ShareFileInfo[];
  childFiles: ShareFileInfo[] | null;
}

// ═══════════════════════════════════
//  Constants
// ═══════════════════════════════════
const API_HOST      = "https://pan.baidu.com";
import config from '../../config';
const CHROMIUM_PATH = config.chromiumPath;
const APP_ID_WEB    = "38824127";  // Web app ID from BaiduPanFilesTransfers

// HTTP headers matching BaiduPanFilesTransfers
const WEB_HEADERS: Record<string, string> = {
  'Host': 'pan.baidu.com',
  'Connection': 'keep-alive',
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  'Accept-Encoding': 'gzip, deflate, br',
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
  'Referer': 'https://pan.baidu.com',
};

function buildHeaders(cookie: string): Record<string, string> {
  if (cookie) {
    return { ...WEB_HEADERS, 'Cookie': cookie };
  }
  return { ...WEB_HEADERS };
}

// ═══════════════════════════════════
//  Playwright singleton for QR login
// ═══════════════════════════════════
let _browser: Browser | null = null;

async function getBrowserSingleton(): Promise<Browser> {
  const { chromium } = await import("playwright");
  if (!_browser || !_browser.isConnected()) {
    _browser = await chromium.launch({
      executablePath: CHROMIUM_PATH,
      headless: true,
      args: [
        "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage",
        "--disable-gpu", "--disable-software-rasterizer",
        "--disable-features=Vulkan", "--use-gl=swiftshader",
      ],
      timeout: 30000,
    });
  }
  return _browser;
}

// ═══════════════════════════════════
//  QR login session store
// ═══════════════════════════════════
const qrSessions = new Map<string, {
  browser: Browser;
  context: BrowserContext;
  page: Page;
  startTime: number;
  verifying: boolean;  // prevent concurrent status checks
}>();

// ═══════════════════════════════════
//  Helpers
// ═══════════════════════════════════

function dailyFolderName(): string {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}

async function humanDelay(): Promise<void> {
  await new Promise(r => setTimeout(r, 500 + Math.random() * 1000));
}

// Extract short_url from share link: /s/1XXXXX... → strip leading '1'
function extractShortUrl(shareUrl: string): { surl: string; pwd: string } | null {
  try {
    const url = new URL(shareUrl);
    if (!url.hostname.includes('pan.baidu.com')) return null;
    const pathMatch = url.pathname.match(/^\/s\/(1[a-zA-Z0-9_-]+)/);
    if (!pathMatch) return null;
    const surl = pathMatch[1].slice(1);
    if (surl.length < 20) return null;
    const pwd = url.searchParams.get('pwd') || '';
    return { surl, pwd };
  } catch {
    return null;
  }
}

// Regex patterns for parsing share page HTML
const RE_SHAREID  = /"shareid":(\d+?),"/;
const RE_SHARE_UK = /"share_uk":"(\d+?)","/;
const RE_FSID     = /"fs_id":(\d+?),"/;
const RE_FILENAME = /"server_filename":"(.+?)","/;
const RE_ISDIR    = /"isdir":(\d+?),"/;
const RE_SIZE     = /"size":(\d+?),"/;
const RE_CATEGORY = /"category":(\d+?),"/;

// ═══════════════════════════════════
//  BaiduDriver
// ═══════════════════════════════════

export class BaiduDriver {
  private config: BaiduConfig;

  constructor(config: BaiduConfig = {}) {
    this.config = { ...config };
  }

  private getCookie(): string {
    return this.config.cookie || '';
  }

  private async getBdstoken(): Promise<string | null> {
    // Use cached if available
    if (this.config.bdstoken) {
      // Validate: bdstoken is typically ~32 chars hex
      if (this.config.bdstoken.length > 10) return this.config.bdstoken;
    }

    const cookie = this.getCookie();
    if (!cookie) return null;

    try {
      const url = `${API_HOST}/api/gettemplatevariable?clienttype=0&app_id=${APP_ID_WEB}&web=1&fields=["bdstoken","token","uk","isdocuser","servertime"]`;
      const res = await fetch(url, {
        headers: buildHeaders(cookie),
        signal: AbortSignal.timeout(10000),
      });
      if (!res.ok) {
        console.error('[Baidu] getBdstoken HTTP', res.status);
        return null;
      }
      const data = await res.json() as any;
      if (data.errno !== 0) {
        console.error('[Baidu] getBdstoken errno:', data.errno);
        // errno -6 = cookie expired / invalid
        if (data.errno === -6) {
          this.config.cookieExpired = true;
          console.error('[Baidu] Cookie expired â user needs to re-scan QR code');
        }
        return null;
      }
      const bdstoken = data.result?.bdstoken || '';
      if (bdstoken) {
        this.config.bdstoken = bdstoken;
        console.log('[Baidu] bdstoken obtained');
        return bdstoken;
      }
      return null;
    } catch (err: any) {
      console.error('[Baidu] getBdstoken error:', err.message);
      return null;
    }
  }

  // ═══════════════════════════════════
  //  QR Login — Playwright browser
  // ═══════════════════════════════════

  static async startQrLogin(): Promise<{ qrUrl?: string; sessionId: string }> {
    const sessionId = "baidu_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
    let browser: Browser | null = null;
    let context: BrowserContext | null = null;
    let page: Page | null = null;

    try {
      browser = await getBrowserSingleton();
      context = await browser.newContext({
        viewport: { width: 1280, height: 800 },
        locale: "zh-CN",
        userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
        ignoreHTTPSErrors: true,
      });
      // Anti-detection
      await context.addInitScript(`(() => { Object.defineProperty(navigator, 'webdriver', { get: () => false }); })()`);

      page = await context.newPage();

      // Navigate directly to passport QR login page (the actual login page with QR code)
      console.log('[BaiduQR] Navigating to passport QR login...');
      await page.goto('https://passport.baidu.com/v2/?login&qrlogin&tpl=netdisk', { waitUntil: 'commit', timeout: 30000 });
      await page.waitForTimeout(4000);

      // Check if we landed on the right page
      const currentUrl = page.url();
      console.log('[BaiduQR] Current URL:', currentUrl);

      // Wait for QR code image on passport page
      console.log('[BaiduQR] Waiting for QR code...');
      await page.waitForSelector('img[src*="passport.baidu.com/v2/api/qrcode"], img[src*="qrcode"]', { timeout: 20000 });
      await page.waitForTimeout(1500);

      // Extract QR code image URL
      const qrImgSrc = await page.evaluate(`(() => {
        const imgs = document.querySelectorAll('img');
        for (const img of imgs) {
          const src = img.src || '';
          if (src.includes('passport.baidu.com/v2/api/qrcode') || (src.includes('qrcode') && img.width > 100)) {
            return src;
          }
        }
        return '';
      })()`) as string;

      if (!qrImgSrc) {
        throw new Error('Could not find QR code image on page');
      }

      // Parse sign + logPage from QR image URL, construct wappass login URL
      let qrContent = '';
      try {
        const imgUrlObj = new URL(qrImgSrc);
        const sign = imgUrlObj.searchParams.get('sign') || '';
        const logPage = imgUrlObj.searchParams.get('logPage') || '';
        const t = Math.floor(Date.now() / 1000);
        qrContent = `https://wappass.baidu.com/wp/?qrlogin&t=${t}&error=0&sign=${sign}&cmd=login&lp=pc&tpl=netdisk&adapter=3&logPage=${encodeURIComponent(logPage)}&qrloginfrom=pc`;
      } catch {
        qrContent = qrImgSrc; // fallback: raw image URL
      }

      // Store session
      qrSessions.set(sessionId, {
        browser: browser,
        context,
        page,
        startTime: Date.now(),
        verifying: false,
      });

      console.log('[BaiduQR] Session stored:', sessionId);
      return { sessionId, qrUrl: qrContent };

    } catch (err: any) {
      console.error('[BaiduQR] startQrLogin error:', err.message);
      // Cleanup on error
      if (page) try { await page.close(); } catch {}
      if (context) try { await context.close(); } catch {}
      throw new Error('启动百度扫码失败: ' + err.message);
    }
    // Note: browser/context/page NOT closed on success — need them for status polling
  }

  static async getQrLoginStatus(sessionId: string): Promise<{
    status: string;
    cookie?: string;
    nickname?: string;
    bdstoken?: string;
    storage_used?: string;
    storage_total?: string;
  }> {
    const session = qrSessions.get(sessionId);
    if (!session) return { status: 'expired' };

    const { page, context, browser } = session;

    // Prevent concurrent status checks (lock)
    if (session.verifying) {
      console.log('[BaiduQR] Status check already in progress, returning pending');
      return { status: 'pending' };
    }
    session.verifying = true;

    // 300s expiry
    if (Date.now() - session.startTime > 300000) {
      try { await context.close(); } catch {}
      session.verifying = false;
      qrSessions.delete(sessionId);
      return { status: 'expired' };
    }

    try {
      // Check cookies for BDUSS (login indicator)
      const cookies = await context.cookies();
      const hasBDUSS = cookies.some((c: any) => {
        if (c.name === 'BDUSS' && c.value && c.value.length > 50) return true;
        return false;
      });

      // Check page for login completion
      const currentUrl = page.url();
      const bodyText = await page.evaluate(`(() => (document.body?.innerText || ''))()`) as string;

      // Login detection signals
      const qrGone = !(await page.$('img[src*="qrcode"]'));
      const loginSuccess = bodyText.includes('登录成功') || bodyText.includes('确认登录');
      const onPanPage = currentUrl.includes('pan.baidu.com/disk');

      if (hasBDUSS && (qrGone || loginSuccess || onPanPage)) {
        // Login detected! Wait for redirect to pan.baidu.com
        console.log('[BaiduQR] Login detected, waiting for redirect...');

        // Wait up to 15s for page to settle on pan.baidu.com
        for (let i = 0; i < 15; i++) {
          await page.waitForTimeout(1000);
          const newUrl = page.url();
          if (newUrl.includes('pan.baidu.com') && !newUrl.includes('passport')) break;
        }

        // Navigate to disk home to ensure cookies are fully set
        try {
          await page.goto('https://pan.baidu.com/disk/home', { waitUntil: 'commit', timeout: 15000 });
          await page.waitForTimeout(2000);
        } catch {}

        // Capture ALL cookies
        const allCookies = await context.cookies();
        const cookieStr = allCookies
          .map((c: any) => `${c.name}=${c.value}`)
          .join('; ');

        console.log(`[BaiduQR] Login success! Got ${allCookies.length} cookies, BDUSS=${hasBDUSS}`);

        // Get nickname and bdstoken
        let nickname = '';
        let bdstoken = '';
        try {
          const bdres = await fetch(
            `${API_HOST}/api/gettemplatevariable?clienttype=0&app_id=${APP_ID_WEB}&web=1&fields=["bdstoken","uk"]`,
            { headers: buildHeaders(cookieStr), signal: AbortSignal.timeout(10000) }
          );
          if (bdres.ok) {
            const bddata = await bdres.json() as any;
            if (bddata.errno === 0) {
              bdstoken = bddata.result?.bdstoken || '';
            }
          }
        } catch {}

        // Get storage info
        let storage_used = '';
        let storage_total = '';
        if (bdstoken) {
          try {
            const qRes = await fetch(`${API_HOST}/api/quota?checkfree=1&checkexpire=1&bdstoken=${bdstoken}`, {
              headers: buildHeaders(cookieStr),
              signal: AbortSignal.timeout(10000),
            });
            if (qRes.ok) {
              const qData = await qRes.json() as any;
              if (qData.errno === 0) {
                const fmt = (bytes: number) => {
                  if (bytes >= 1024**4) return (bytes / 1024**4).toFixed(2) + ' TB';
                  if (bytes >= 1024**3) return (bytes / 1024**3).toFixed(2) + ' GB';
                  if (bytes >= 1024**2) return (bytes / 1024**2).toFixed(2) + ' MB';
                  return (bytes / 1024).toFixed(2) + ' KB';
                };
                storage_used = qData.used ? fmt(qData.used) : '';
                storage_total = qData.total ? fmt(qData.total) : '';
              }
            }
          } catch {}
        }

        // Get nickname from Baidu REST API (baidu_name field)
        if (bdstoken) {
          try {
            const uRes = await fetch(`${API_HOST}/rest/2.0/xpan/nas?method=uinfo`, {
              headers: buildHeaders(cookieStr),
              signal: AbortSignal.timeout(10000),
            });
            if (uRes.ok) {
              const uData = await uRes.json() as any;
              if (uData.errno === 0 && uData.baidu_name) {
                nickname = uData.baidu_name;
              }
            }
          } catch {}
        }

        // Cleanup
        session.verifying = false;
        try { await context.close(); } catch {}
        qrSessions.delete(sessionId);

        return {
          status: 'logged_in',
          cookie: cookieStr,
          nickname: nickname || '百度用户',
          bdstoken,
          storage_used,
          storage_total,
        };
      }

      // Still pending
      session.verifying = false;
      return { status: 'pending' };

    } catch (err: any) {
      console.error('[BaiduQR] Status check error:', err.message);
      session.verifying = false;
      return { status: 'pending' };
    }
  }

  static cancelQrLogin(sessionId: string) {
    const session = qrSessions.get(sessionId);
    if (session) {
      const { context } = session;
      try { context.close(); } catch {}
      qrSessions.delete(sessionId);
      console.log('[BaiduQR] Cancelled:', sessionId);
    }
  }

  // ═══════════════════════════════════
  //  Validate — check cookie validity
  // ═══════════════════════════════════

  async validate(): Promise<boolean> {
    const cookie = this.getCookie();
    if (!cookie || !cookie.includes('BDUSS')) return false;
    const bdstoken = await this.getBdstoken();
    return bdstoken !== null;
  }

  async getUserInfo(): Promise<{ nickname: string; usedBytes: number; totalBytes: number } | null> {
    const cookie = this.getCookie();
    if (!cookie) return null;

    try {
      let nickname = this.config.nickname || '';
      let usedBytes = 0;
      let totalBytes = 0;

      // Try to get user info from /api/userinfo
      const uRes = await fetch(`${API_HOST}/api/userinfo?act=getuserinfo&bdstoken=${await this.getBdstoken()}`, {
        headers: buildHeaders(cookie),
        signal: AbortSignal.timeout(10000),
      });
      if (uRes.ok) {
        const uData = await uRes.json() as any;
        if (uData.errno === 0 && uData.records) {
          nickname = uData.records[0]?.username || nickname;
        }
      }

      // Get quota
      try {
        const qRes = await fetch(`${API_HOST}/api/quota?checkfree=1&checkexpire=1&bdstoken=${await this.getBdstoken()}`, {
          headers: buildHeaders(cookie),
          signal: AbortSignal.timeout(10000),
        });
        if (qRes.ok) {
          const qData = await qRes.json() as any;
          if (qData.errno === 0) {
            usedBytes = qData.used || 0;
            totalBytes = qData.total || 0;
          }
        }
      } catch {}

      return { nickname, usedBytes, totalBytes };
    } catch {
      return null;
    }
  }

  async getStorageInfo(): Promise<{ used: string; total: string }> {
    const info = await this.getUserInfo();
    if (!info) return { used: '0 B', total: '0 B' };
    const fmt = (bytes: number) => {
      if (bytes >= 1024**4) return (bytes / 1024**4).toFixed(2) + ' TB';
      if (bytes >= 1024**3) return (bytes / 1024**3).toFixed(2) + ' GB';
      if (bytes >= 1024**2) return (bytes / 1024**2).toFixed(2) + ' MB';
      return (bytes / 1024).toFixed(2) + ' KB';
    };
    return { used: fmt(info.usedBytes), total: fmt(info.totalBytes) };
  }

  // ═══════════════════════════════════
  //  File list (Cookie-based)
  // ═══════════════════════════════════

  private async listRootDir(): Promise<Array<{ fid: string; file_name: string; dir: boolean; size: number }>> {
    const cookie = this.getCookie();
    if (!cookie) return [];
    const bdstoken = await this.getBdstoken();
    if (!bdstoken) return [];

    try {
      const url = `${API_HOST}/api/list?order=time&desc=1&showempty=0&web=1&page=1&num=1000&dir=/&bdstoken=${bdstoken}`;
      const res = await fetch(url, {
        headers: buildHeaders(cookie),
        signal: AbortSignal.timeout(15000),
      });
      if (!res.ok) return [];
      const data = await res.json() as any;
      if (data.errno === 0 && data.list) {
        return data.list.map((f: any) => ({
          fid: String(f.fs_id),
          file_name: f.server_filename,
          dir: f.isdir === 1 || f.isdir === '1',
          size: f.size || 0,
        }));
      }
      console.error('[Baidu] listRootDir errno:', data.errno);
      return [];
    } catch (err: any) {
      console.error('[Baidu] listRootDir error:', err.message);
      return [];
    }
  }

  private async createDir(path: string): Promise<boolean> {
    const cookie = this.getCookie();
    if (!cookie) return false;
    const bdstoken = await this.getBdstoken();
    if (!bdstoken) return false;

    try {
      const url = `${API_HOST}/api/create?a=commit&bdstoken=${bdstoken}`;
      const body = new URLSearchParams({ path, isdir: '1', block_list: '[]' });
      const res = await fetch(url, {
        method: 'POST',
        headers: { ...buildHeaders(cookie), 'Content-Type': 'application/x-www-form-urlencoded' },
        body: body.toString(),
        signal: AbortSignal.timeout(15000),
      });
      if (!res.ok) return false;
      const data = await res.json() as any;
      if (data.errno === 0) return true;
      if (data.errno === -8) return true; // already exists
      console.error('[Baidu] createDir errno:', data.errno);
      return false;
    } catch (err: any) {
      console.error('[Baidu] createDir error:', err.message);
      return false;
    }
  }

  private async findOrCreateDir(dirName: string): Promise<string | null> {
    const rootItems = await this.listRootDir();
    const existing = rootItems.find(f => f.file_name === dirName && f.dir);
    if (existing) return `/${dirName}`;

    const ok = await this.createDir(`/${dirName}`);
    if (ok) {
      console.log(`[Baidu] Created dir: ${dirName}`);
      return `/${dirName}`;
    }
    return null;
  }

  // ═══════════════════════════════════
  //  Delete files
  // ═══════════════════════════════════

  private async deleteFiles(fsIds: string[]): Promise<boolean> {
    const cookie = this.getCookie();
    if (!cookie) return false;
    const bdstoken = await this.getBdstoken();
    if (!bdstoken) return false;

    try {
      const filelist = JSON.stringify(fsIds);
      const body = new URLSearchParams({ async: '2', filelist });
      const res = await fetch(`${API_HOST}/api/filemanager?opera=delete&bdstoken=${bdstoken}`, {
        method: 'POST',
        headers: { ...buildHeaders(cookie), 'Content-Type': 'application/x-www-form-urlencoded' },
        body: body.toString(),
        signal: AbortSignal.timeout(30000),
      });
      if (!res.ok) return false;
      const data = await res.json() as any;
      return data.errno === 0;
    } catch (err: any) {
      console.error('[Baidu] deleteFiles error:', err.message);
      return false;
    }
  }

  // ═══════════════════════════════════
  //  Validate share link
  // ═══════════════════════════════════

  async validateShareLink(shareUrl: string): Promise<{ valid: boolean; message: string; fileCount?: number }> {
    const parsed = extractShortUrl(shareUrl);
    if (!parsed) return { valid: false, message: '无法解析百度网盘链接格式' };
    const { surl, pwd } = parsed;

    try {
      // Try to get share file list
      const shareInfo = await this.getShareFiles(surl, pwd);
      if (shareInfo && shareInfo.files.length > 0) {
        return { valid: true, message: `有效（${shareInfo.files.length} 个文件）`, fileCount: shareInfo.files.length };
      }
      return { valid: false, message: '链接已过期或需要提取码' };
    } catch (err: any) {
      return { valid: false, message: `验证失败: ${err.message}` };
    }
  }

  // ═══════════════════════════════════
  //  Get share file list — Cookie HTTP
  // ═══════════════════════════════════
  // Flow: getbdstoken → verify password (get randsk) → update cookie with BDCLND → GET share page → regex parse

  private async getShareFiles(surl: string, pwd: string): Promise<ShareDetail | null> {
    const cookie = this.getCookie();
    if (!cookie) {
      console.log('[Baidu] No cookie available for share file listing');
      return null;
    }

    const bdstoken = await this.getBdstoken();
    if (!bdstoken) {
      console.log('[Baidu] No bdstoken available');
      return null;
    }

    let workingCookie = cookie;

    try {
      // Step 1: Verify password and get randsk (BDCLND)
      if (pwd) {
        console.log(`[Baidu:Share] Verifying password for surl=${surl}...`);
        const t = String(Date.now());
        const verifyUrl = `${API_HOST}/share/verify?surl=${surl}&bdstoken=${bdstoken}&t=${t}&channel=chunlei&web=1&clienttype=0`;
        const verifyBody = new URLSearchParams({ pwd, vcode: '', vcode_str: '' });

        const vRes = await fetch(verifyUrl, {
          method: 'POST',
          headers: { ...buildHeaders(workingCookie), 'Content-Type': 'application/x-www-form-urlencoded' },
          body: verifyBody.toString(),
          signal: AbortSignal.timeout(10000),
        });

        if (!vRes.ok) {
          console.log(`[Baidu:Share] Password verify HTTP ${vRes.status}`);
          return null;
        }
        const vData = await vRes.json() as any;

        if (vData.errno !== 0) {
          // Error codes: -9/-12 = wrong password, 105 = not found, -62 = blocked
          const errMap: Record<number|string, string> = { '-9': '提取码错误', '-12': '提取码错误', '105': '链接不存在', '-62': '访问次数过多', '2': '链接已过期', '-33': '转存失败' };
          console.log(`[Baidu:Share] Password verify failed: errno=${vData.errno} — ${errMap[vData.errno] || '未知错误'}`);
          return null;
        }

        const randsk = vData.randsk || '';
        if (randsk) {
          // Update cookie with BDCLND=randsk (per BaiduPanFilesTransfers)
          workingCookie = updateCookie(workingCookie, 'BDCLND', randsk);
          console.log('[Baidu:Share] Password verified, BDCLND updated');
        }
      }

      // Step 2: GET share page with updated cookie
      const shareUrl = `https://pan.baidu.com/s/1${surl}`;
      console.log(`[Baidu:Share] Fetching share page: ${shareUrl}`);
      const sRes = await fetch(shareUrl, {
        headers: buildHeaders(workingCookie),
        signal: AbortSignal.timeout(15000),
        redirect: 'follow',
      });

      if (!sRes.ok) {
        console.log(`[Baidu:Share] Share page HTTP ${sRes.status}`);
        return null;
      }

      const html = await sRes.text();

      // Check for error states
      if (html.includes('页面不存在') || html.includes('你来晚了') || html.includes('链接已失效') || html.includes('分享已过期')) {
        console.log('[Baidu:Share] Share link is dead/expired');
        return null;
      }

      // Step 3: Parse HTML for file info using regex
      const shareidMatch = html.match(RE_SHAREID);
      const ukMatch = html.match(RE_SHARE_UK);
      const fsIdMatches = [...html.matchAll(new RegExp(RE_FSID.source, 'g'))];
      const filenameMatches = [...html.matchAll(new RegExp(RE_FILENAME.source, 'g'))];
      const isdirMatches = [...html.matchAll(new RegExp(RE_ISDIR.source, 'g'))];
      const sizeMatches = [...html.matchAll(new RegExp(RE_SIZE.source, 'g'))];
      const categoryMatches = [...html.matchAll(new RegExp(RE_CATEGORY.source, 'g'))];

      if (!shareidMatch || !ukMatch || fsIdMatches.length === 0) {
        // Try alternative extraction from yunData in script tags
        const yunMatch = html.match(/yunData\.setData\((\{[^]*?\})\);?/);
        if (yunMatch) {
          try {
            const yunData = JSON.parse(yunMatch[1]);
            if (yunData.filelist && yunData.filelist.length > 0) {
              const files: ShareFileInfo[] = yunData.filelist.map((f: any) => ({
                server_filename: f.server_filename || '',
                fs_id: String(f.fs_id),
                isdir: f.isdir || 0,
                size: f.size || 0,
                path: f.path || '',
                category: f.category || 0,
              }));
              console.log(`[Baidu:Share] Found ${files.length} file(s) via yunData`);
              return { files, childFiles: null };
            }
          } catch {}
        }

        console.log('[Baidu:Share] Could not parse file list from page');
        return null;
      }

      const count = fsIdMatches.length;
      const files: ShareFileInfo[] = [];
      for (let i = 0; i < count; i++) {
        files.push({
          server_filename: filenameMatches[i] ? filenameMatches[i][1] : '',
          fs_id: fsIdMatches[i][1],
          isdir: isdirMatches[i] ? parseInt(isdirMatches[i][1]) : 0,
          size: sizeMatches[i] ? parseInt(sizeMatches[i][1]) : 0,
          path: '',
          category: categoryMatches[i] ? parseInt(categoryMatches[i][1]) : 0,
        });
      }

      console.log(`[Baidu:Share] Found ${files.length} file(s) via regex parse`);
      return { files, childFiles: null };

    } catch (err: any) {
      console.error('[Baidu:Share] getShareFiles error:', err.message);
      return null;
    }
  }

  // ═══════════════════════════════════
  //  Transfer files — Cookie HTTP
  // ═══════════════════════════════════

  private async transferFiles(
    surl: string, pwd: string,
    fsIds: string[], destPath: string
  ): Promise<{ success: boolean; taskId?: string; message: string }> {
    const cookie = this.getCookie();
    if (!cookie) return { success: false, message: '未登录百度网盘' };

    const bdstoken = await this.getBdstoken();
    if (!bdstoken) return { success: false, message: '获取 bdstoken 失败，Cookie 可能已过期' };

    let workingCookie = cookie;

    try {
      // Step 1: Get share info from page (shareid + uk)
      const shareUrl = `https://pan.baidu.com/s/1${surl}`;

      // Verify password first if needed
      if (pwd) {
        const t = String(Date.now());
        const vUrl = `${API_HOST}/share/verify?surl=${surl}&bdstoken=${bdstoken}&t=${t}&channel=chunlei&web=1&clienttype=0`;
        const vBody = new URLSearchParams({ pwd, vcode: '', vcode_str: '' });

        const vRes = await fetch(vUrl, {
          method: 'POST',
          headers: { ...buildHeaders(workingCookie), 'Content-Type': 'application/x-www-form-urlencoded' },
          body: vBody.toString(),
          signal: AbortSignal.timeout(10000),
        });

        if (vRes.ok) {
          const vData = await vRes.json() as any;
          if (vData.errno === 0 && vData.randsk) {
            workingCookie = updateCookie(workingCookie, 'BDCLND', vData.randsk);
          } else {
            return { success: false, message: `密码验证失败 errno=${vData.errno}` };
          }
        }
      }

      // Get share page to extract shareid + uk
      const sRes = await fetch(shareUrl, {
        headers: buildHeaders(workingCookie),
        signal: AbortSignal.timeout(15000),
        redirect: 'follow',
      });
      if (!sRes.ok) return { success: false, message: `无法访问分享页面 HTTP ${sRes.status}` };

      const html = await sRes.text();
      const shareidMatch = html.match(RE_SHAREID);
      const ukMatch = html.match(RE_SHARE_UK);

      if (!shareidMatch || !ukMatch) {
        return { success: false, message: '无法从页面提取分享信息' };
      }

      const shareid = shareidMatch[1];
      const uk = ukMatch[1];

      // Step 2: Transfer
      console.log(`[Baidu:Transfer] Transferring ${fsIds.length} file(s) to ${destPath}...`);
      const fsidlist = `[${fsIds.join(',')}]`;
      const path = destPath === '/' ? '/' : `/${destPath.replace(/^\//, '')}`;

      const tUrl = `${API_HOST}/share/transfer?shareid=${shareid}&from=${uk}&bdstoken=${bdstoken}&channel=chunlei&web=1&clienttype=0`;
      const tBody = new URLSearchParams({ fsidlist, path });

      // Retry up to 3 times for transient fetch failures
      let tRes: any;
      let lastErr: any;
      for (let attempt = 0; attempt < 3; attempt++) {
        try {
          tRes = await fetch(tUrl, {
            method: 'POST',
            headers: { ...buildHeaders(workingCookie), 'Content-Type': 'application/x-www-form-urlencoded' },
            body: tBody.toString(),
            signal: AbortSignal.timeout(30000),
          });
          break;
        } catch (err: any) {
          lastErr = err;
          if (attempt < 2) {
            console.log(`[Baidu:Transfer] Attempt ${attempt + 1} failed: ${err.message}, retrying in 2s...`);
            await new Promise(r => setTimeout(r, 2000));
          }
        }
      }
      if (!tRes) return { success: false, message: `转存网络错误: ${lastErr?.message || 'fetch failed'}` };

      if (!tRes.ok) return { success: false, message: `转存请求失败 HTTP ${tRes.status}` };

      const tData = await tRes.json() as any;

      if (tData.errno === 0) {
        console.log(`[Baidu:Transfer] Success!`);
        return { success: true, taskId: `transfer_${Date.now()}`, message: 'ok' };
      }

      // Known error codes
      const errMap: Record<number, string> = {
        0: '转存成功',
        2: '目标目录不存在',
        4: '目录中存在同名文件',
        12: '转存文件数超过限制',
        20: '容量不足',
        '-4': '登录失效，请重新登录',
        '-6': 'Cookie 无效，请重新获取',
        '-62': '访问次数过多，请稍后再试',
        // -33 is a known error, mapped as generic
      };

      const errMsg = errMap[tData.errno] || `errno=${tData.errno}`;
      console.error(`[Baidu:Transfer] Failed: ${errMsg}`);
      return { success: false, message: errMsg };

    } catch (err: any) {
      console.error('[Baidu:Transfer] Error:', err.message);
      return { success: false, message: `转存异常: ${err.message}` };
    }
  }

  // ═══════════════════════════════════
  //  Main saveFromShare — full pipeline
  // ═══════════════════════════════════

  async saveFromShare(shareUrl: string, sourceTitle?: string): Promise<{
    success: boolean;
    message: string;
    shareUrl?: string;
    sharePwd?: string;
    folderName?: string;
    taskId?: string;
    fileCount?: number;
    folderCount?: number;
    originalFolderName?: string;
  }> {
    const parsed = extractShortUrl(shareUrl);
    if (!parsed) return { success: false, message: '无法解析百度网盘链接格式' };

    const { surl, pwd } = parsed;
    console.log(`[Baidu] saveFromShare: surl=${surl}, pwd=${pwd ? '***' : '(none)'}`);

    // Step 1: Get share file list
    const shareInfo = await this.getShareFiles(surl, pwd);
    if (!shareInfo || shareInfo.files.length === 0) {
      if ((this.config as any).cookieExpired) {
        return {
          success: false,
          message: '百度登录已过期，请重新扫码登录',
          cookieExpired: true,
        } as any;
      }
      return { success: false, message: '获取分享文件列表失败，链接可能已过期或需要提取码' };
    }

    const { files } = shareInfo;
    const originalFolderName = files[0]?.server_filename || '';
    const fileCount = files.filter(f => !f.isdir).length;
    const folderCount = files.filter(f => f.isdir).length;

    // Step 2: Create/find daily folder, then sub-folder with original name
    await humanDelay();
    const saveDirName = dailyFolderName();
    console.log(`[Baidu] Creating/finding dir: ${saveDirName}`);
    const saveDirPath = await this.findOrCreateDir(saveDirName);
    let destPath = saveDirPath || '/';
    if (!saveDirPath) {
      console.log(`[Baidu] WARNING: failed to create dir, saving to root`);
    }

    // Create sub-folder with original name under date folder
    let savedFolderName = saveDirName;
    if (originalFolderName && saveDirPath) {
      const subDirName = originalFolderName.replace(/[/\\:*?"<>|]/g, '_').substring(0, 100);
      const subDirPath = `${saveDirPath}/${subDirName}`;
      const subOk = await this.createDir(subDirPath);
      if (subOk) {
        destPath = subDirPath;
        savedFolderName = `${saveDirName}/${subDirName}`;
        console.log(`[Baidu] Created sub-folder: ${subDirName}`);
      } else {
        console.log(`[Baidu] Failed to create sub-folder, saving to ${destPath}`);
      }
    }

    // Step 3: Transfer files
    await humanDelay();
    const fsIds = files.map(f => f.fs_id);
    console.log(`[Baidu] Transferring ${fsIds.length} file(s) to ${destPath}`);
    const transferResult = await this.transferFiles(surl, pwd, fsIds, destPath);
    if (!transferResult.success) {
      return { success: false, message: `转存失败: ${transferResult.message}`, fileCount, folderCount, originalFolderName };
    }

    console.log(`[Baidu] Save complete: ${fsIds.length} files -> ${destPath}`);

    // Step 4: Create share link from user's own drive
    let ownShareUrl = '';
    let ownSharePwd = '';
    let shareMsg = '';
    try {
      // Find the saved directory to get its fs_id for sharing
      const savedDir = await this.findDirByPath(destPath);
      if (savedDir) {
        const shareResult = await this.createShareLink(savedDir);
        if (shareResult.success && shareResult.shareUrl) {
          ownShareUrl = shareResult.shareUrl;
          ownSharePwd = shareResult.sharePwd || '';
          shareMsg = '（已创建分享链接）';
        } else if (shareResult.needVerify) {
          // Account needs verification to create shares
          shareMsg = '（你的百度账号需要实名/绑定手机才能创建分享链接，当前为源链接）';
          ownShareUrl = `https://pan.baidu.com/s/1${surl}`;
          ownSharePwd = pwd || '';
        } else {
          shareMsg = `（分享创建失败：${shareResult.message}，当前为源链接）`;
          ownShareUrl = `https://pan.baidu.com/s/1${surl}`;
          ownSharePwd = pwd || '';
        }
      } else {
        ownShareUrl = `https://pan.baidu.com/s/1${surl}`;
        ownSharePwd = pwd || '';
      }
    } catch {
      ownShareUrl = `https://pan.baidu.com/s/1${surl}`;
      ownSharePwd = pwd || '';
    }

    return {
      success: true,
      message: `✅ 转存成功${shareMsg}`,
      shareUrl: ownShareUrl || undefined,
      sharePwd: ownSharePwd || undefined,
      folderName: savedFolderName,
      taskId: transferResult.taskId,
      fileCount,
      folderCount,
      originalFolderName,
    };
  }

  // ═══════════════════════════════════
  //  Share creation (user's own drive)
  // ═══════════════════════════════════

  /** Find a directory by path, return its fs_id */
  private async findDirByPath(dirPath: string): Promise<string | null> {
    const cookie = this.getCookie();
    if (!cookie) return null;
    const bdstoken = await this.getBdstoken();
    if (!bdstoken) return null;

    const parentPath = dirPath.substring(0, dirPath.lastIndexOf('/')) || '/';
    const dirName = dirPath.substring(dirPath.lastIndexOf('/') + 1);

    try {
      const res = await fetch(
        `${API_HOST}/api/list?dir=${encodeURIComponent(parentPath)}&bdstoken=${bdstoken}&order=time&desc=1`,
        { headers: buildHeaders(cookie), signal: AbortSignal.timeout(10000) }
      );
      if (!res.ok) return null;
      const data = await res.json() as any;
      if (data.errno !== 0) return null;
      const found = (data.list || []).find((f: any) => f.server_filename === dirName && f.isdir === 1);
      return found ? String(found.fs_id) : null;
    } catch {
      return null;
    }
  }

  /** Create a share link from a file/directory in user's own drive */
  private async createShareLink(fsId: string): Promise<{ success: boolean; shareUrl?: string; sharePwd?: string; message: string; needVerify?: boolean }> {
    const cookie = this.getCookie();
    if (!cookie) return { success: false, message: '未登录' };
    const bdstoken = await this.getBdstoken();
    if (!bdstoken) return { success: false, message: '获取 bdstoken 失败' };

    try {
      // Generate a random 4-char share password (required by Baidu share/set API)
      const pwd = Math.random().toString(36).substring(2, 6);
      const body = new URLSearchParams({
        fid_list: `[${fsId}]`,
        schannel: '0',
        channel_list: '[]',
        period: '0',
        pwd,
      });

      const url = `${API_HOST}/share/set?bdstoken=${bdstoken}&channel=chunlei&web=1&clienttype=0&app_id=250528`;
      const res = await fetch(url, {
        method: 'POST',
        headers: { ...buildHeaders(cookie), 'Content-Type': 'application/x-www-form-urlencoded' },
        body: body.toString(),
        signal: AbortSignal.timeout(15000),
      });

      const data = await res.json() as any;

      if (data.errno === 0) {
        // Success — extract shareid and link from response
        const shareid = data.shareid;
        if (shareid) {
          const link = data.link || `https://pan.baidu.com/s/1${shareid}`;
          return { success: true, shareUrl: link, sharePwd: pwd, message: 'ok' };
        }
        return { success: false, message: '创建成功但未获取到分享链接' };
      }

      if (data.errno === 115) {
        // Account genuinely restricted (should not happen with correct pwd param)
        return { success: false, message: '账号异常，禁止分享', needVerify: true };
      }

      return { success: false, message: data.show_msg || `分享创建失败 errno=${data.errno}` };
    } catch (err: any) {
      return { success: false, message: err.message || '网络错误' };
    }
  }

  // ═══════════════════════════════════
  //  Cleanup
  // ═══════════════════════════════════

  async emptyTrash(): Promise<boolean> {
    // Cookie-based approach: list recycle and delete
    const cookie = this.getCookie();
    if (!cookie) return false;

    try {
      const bdstoken = await this.getBdstoken();
      if (!bdstoken) return false;

      // We don't have a dedicated trash API with Cookie, use /api/list with recycle parameter
      // For now, skip if trash is empty
      console.log('[Baidu] emptyTrash: not fully implemented for Cookie yet, skipping');
      return true;
    } catch (err: any) {
      console.error('[Baidu] emptyTrash error:', err.message);
      return false;
    }
  }

  async cleanupOldDateFolders(days: number, _whitelistDirs?: string[]): Promise<{ trashed: number; errors: string[] }> {
    const errors: string[] = [];
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - days);
    const cutoffStr = cutoff.toISOString().slice(0, 10);

    try {
      const rootItems = await this.listRootDir();
      const oldFolders = rootItems.filter(item => {
        if (!item.dir) return false;
        if (!/^\d{4}-\d{2}-\d{2}$/.test(item.file_name)) return false;
        return item.file_name < cutoffStr;
      });

      if (oldFolders.length === 0) return { trashed: 0, errors: [] };

      const fsIds = oldFolders.map(f => f.fid);
      console.log(`[Baidu] Deleting ${fsIds.length} old date folders (before ${cutoffStr}): ${oldFolders.map(f => f.file_name).join(', ')}`);
      const ok = await this.deleteFiles(fsIds);
      if (ok) return { trashed: fsIds.length, errors: [] };
      return { trashed: 0, errors: [`删除 ${fsIds.length} 个文件夹失败`] };
    } catch (err: any) {
      return { trashed: 0, errors: [err.message] };
    }
  }

  async cleanupBySpaceThreshold(thresholdPercent: number, deletePercent: number, _whitelistDirs?: string[]): Promise<{ trashed: number; errors: string[] }> {
    const errors: string[] = [];
    try {
      const info = await this.getUserInfo();
      if (!info || info.totalBytes <= 0) return { trashed: 0, errors: [] };

      const usagePercent = (info.usedBytes / info.totalBytes) * 100;
      if (usagePercent < thresholdPercent) {
        console.log(`[Baidu] Usage ${usagePercent.toFixed(1)}% below threshold ${thresholdPercent}%, skipping`);
        return { trashed: 0, errors: [] };
      }

      const targetBytesToFree = Math.floor(info.totalBytes * Math.min(deletePercent, 100) / 100);
      const rootItems = await this.listRootDir();
      const dateFolders = rootItems
        .filter(item => item.dir && /^\d{4}-\d{2}-\d{2}$/.test(item.file_name))
        .sort((a, b) => a.file_name.localeCompare(b.file_name));

      if (dateFolders.length === 0) return { trashed: 0, errors: [] };

      const avgSize = info.usedBytes / dateFolders.length;
      const estCount = Math.max(1, Math.ceil(targetBytesToFree / (avgSize || 1)));
      const foldersToTrash = dateFolders.slice(0, Math.min(estCount, dateFolders.length));

      const freedMB = (foldersToTrash.length * (avgSize || 0) / 1024 / 1024).toFixed(0);
      const fsIdsToTrash = foldersToTrash.map(f => f.fid);
      console.log(`[Baidu] Space threshold: deleting ${foldersToTrash.length}/${dateFolders.length} oldest folders (~${freedMB} MB)`);

      const ok = await this.deleteFiles(fsIdsToTrash);
      if (ok) return { trashed: foldersToTrash.length, errors: [] };
      return { trashed: 0, errors: ['空间阈值清理失败'] };
    } catch (err: any) {
      return { trashed: 0, errors: [err.message] };
    }
  }
}

// ═══════════════════════════════════
//  Utility: update Cookie string
// ═══════════════════════════════════

function updateCookie(cookieStr: string, key: string, value: string): string {
  const pairs = cookieStr.split(';').map(s => s.trim()).filter(s => s);
  let found = false;
  const updated = pairs.map(p => {
    const eq = p.indexOf('=');
    if (eq > 0 && p.substring(0, eq) === key) {
      found = true;
      return `${key}=${value}`;
    }
    return p;
  });
  if (!found) {
    updated.push(`${key}=${value}`);
  }
  return updated.join('; ');
}
