/**
 * Douyin draft — upload through the official creator page and save as draft.
 *
 * The previous API pipeline relied on an old pre-upload endpoint that no longer
 * matches creator center's live upload flow. This command now drives the
 * official upload page directly so it stays aligned with the site.
 */
import * as fs from 'node:fs';
import * as path from 'node:path';
import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
const VISIBILITY_LABELS = {
    public: '公开',
    friends: '好友可见',
    private: '仅自己可见',
};
const DRAFT_UPLOAD_URL = 'https://creator.douyin.com/creator-micro/content/upload';
const COMPOSER_WAIT_ATTEMPTS = 120;
const COVER_INPUT_WAIT_ATTEMPTS = 20;
const COVER_READY_WAIT_ATTEMPTS = 20;
/**
 * Best-effort dismissal for coach marks and upload tips that can block clicks.
 */
async function dismissKnownModals(page) {
    await page.evaluate(`() => {
    const targets = ['我知道了', '知道了', '关闭'];
    for (const text of targets) {
      const btn = Array.from(document.querySelectorAll('button,[role="button"]'))
        .find((el) => (el.textContent || '').trim() === text);
      if (btn instanceof HTMLElement) btn.click();
    }
  }`);
}
/**
 * Wait until Douyin finishes uploading and lands on the post-video composer.
 */
async function waitForDraftComposer(page) {
    let lastState = {
        href: '',
        ready: false,
        bodyText: '',
    };
    for (let attempt = 0; attempt < COMPOSER_WAIT_ATTEMPTS; attempt += 1) {
        lastState = (await page.evaluate(`() => ({
      href: location.href,
      ready: !!Array.from(document.querySelectorAll('input')).find(
        (el) => (el.placeholder || '').includes('填写作品标题')
      ) && !!Array.from(document.querySelectorAll('button')).find(
        (el) => (el.textContent || '').includes('暂存离开')
      ),
      bodyText: document.body?.innerText || ''
    })`));
        if (lastState.ready)
            return;
        await page.wait({ time: 0.5 });
    }
    throw new CommandExecutionError('等待抖音草稿编辑页超时', `当前页面: ${lastState.href || 'unknown'}`);
}
/**
 * Fill title, caption and visibility controls on the live composer page.
 */
async function fillDraftComposer(page, options) {
    const titleOk = (await page.evaluate(`() => {
    const titleInput = Array.from(document.querySelectorAll('input')).find(
      (el) => (el.placeholder || '').includes('填写作品标题')
    );
    if (!(titleInput instanceof HTMLInputElement)) return false;
    const propKey = Object.keys(titleInput).find((key) => key.startsWith('__reactProps$'));
    const props = propKey ? titleInput[propKey] : null;
    if (props?.onChange) {
      props.onChange({
        target: { value: ${JSON.stringify(options.title)} },
        currentTarget: { value: ${JSON.stringify(options.title)} },
      });
    } else {
      titleInput.focus();
      titleInput.value = ${JSON.stringify(options.title)};
      titleInput.dispatchEvent(new Event('input', { bubbles: true }));
      titleInput.dispatchEvent(new Event('change', { bubbles: true }));
    }
    if (props?.onBlur) {
      props.onBlur({
        target: titleInput,
        currentTarget: titleInput,
        relatedTarget: null,
      });
    } else {
      titleInput.dispatchEvent(new Event('blur', { bubbles: true }));
    }
    return true;
  }`));
    if (!titleOk) {
        throw new CommandExecutionError('填写抖音草稿表单失败: title-input-missing');
    }
    if (options.caption) {
        const captionOk = (await page.evaluate(`() => {
      const editor = document.querySelector('[contenteditable="true"]');
      if (!(editor instanceof HTMLElement)) return false;
      editor.focus();
      editor.textContent = '';
      document.execCommand('selectAll', false);
      document.execCommand('insertText', false, ${JSON.stringify(options.caption)});
      editor.dispatchEvent(new Event('input', { bubbles: true }));
      return true;
    }`));
        if (!captionOk) {
            throw new CommandExecutionError('填写抖音草稿表单失败: caption-editor-missing');
        }
    }
    const visibilityOk = (await page.evaluate(`() => {
    const visibility = Array.from(document.querySelectorAll('label')).find(
      (el) => (el.textContent || '').includes(${JSON.stringify(options.visibilityLabel)})
    );
    if (!(visibility instanceof HTMLElement)) return false;
    visibility.click();
    return true;
  }`));
    if (!visibilityOk) {
        throw new CommandExecutionError('填写抖音草稿表单失败: visibility-missing');
    }
}
/**
 * Switch the composer into custom-cover mode and expose the cover input with a
 * stable selector for CDP file injection.
 */
async function prepareCustomCoverInput(page) {
    let lastReason = 'cover-input-missing';
    const baselineCount = (await page.evaluate(`() => Array.from(document.querySelectorAll('input[type="file"]')).length`));
    for (let attempt = 0; attempt < COVER_INPUT_WAIT_ATTEMPTS; attempt += 1) {
        const result = (await page.evaluate(`() => {
      const coverLabel = Array.from(document.querySelectorAll('label')).find(
        (el) => (el.textContent || '').includes('上传新封面')
      );
      if (coverLabel instanceof HTMLElement) {
        coverLabel.click();
      }

      const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
      const target = inputs
        .slice(${JSON.stringify(baselineCount)})
        .find((el) => el instanceof HTMLInputElement && !el.disabled);
      if (!(target instanceof HTMLInputElement)) {
        return { ok: false, reason: 'cover-input-pending' };
      }

      document
        .querySelectorAll('[data-opencli-cover-input="1"]')
        .forEach((el) => el.removeAttribute('data-opencli-cover-input'));
      target.setAttribute('data-opencli-cover-input', '1');
      return { ok: true, selector: '[data-opencli-cover-input="1"]' };
    }`));
        if (result?.ok && result.selector) {
            return result.selector;
        }
        lastReason = result?.reason || lastReason;
        await page.wait({ time: 0.5 });
    }
    throw new CommandExecutionError(`准备抖音自定义封面输入框失败: ${lastReason}`);
}
/**
 * Read the local quick-check panel text that reflects cover validation state.
 */
export function buildCoverCheckPanelTextJs() {
    return `() => {
    const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
    const stateTexts = ['检测', '检测中', '封面检测中', '重新检测', '横/竖双封面缺失'];
    const marker = Array.from(document.querySelectorAll('div,span,p,button')).find(
      (el) => normalize(el.textContent) === '快速检测'
    );
    let root = marker?.parentElement || null;
    while (root && root !== document.body) {
      const descendants = Array.from(root.querySelectorAll('div,span,p,button'))
        .map((el) => normalize(el.textContent));
      const hasMarkerText = descendants.includes('快速检测');
      const hasStateText = descendants.some((text) => stateTexts.includes(text));
      if (hasMarkerText && hasStateText) {
        return normalize(root.textContent).slice(0, 400);
      }
      root = root.parentElement;
    }
    return '';
  }`;
}
async function getCoverCheckPanelText(page) {
    return (await page.evaluate(buildCoverCheckPanelTextJs())) || '';
}
/**
 * Wait for Douyin's cover-detection pipeline to expose a post-upload signal.
 * In the live creator page, custom cover upload first shows `封面检测中`, then
 * lands on a ready state such as `重新检测` or the warning copy for missing
 * horizontal/vertical covers.
 */
async function waitForCoverReady(page) {
    let lastPanelText = '';
    let sawBusy = false;
    for (let attempt = 0; attempt < COVER_READY_WAIT_ATTEMPTS; attempt += 1) {
        const panelText = await getCoverCheckPanelText(page);
        const busy = panelText.includes('检测中');
        const ready = (panelText.includes('重新检测')
            || panelText.includes('横/竖双封面缺失'));
        if (busy) {
            sawBusy = true;
        }
        if (sawBusy && ready && !busy) {
            return;
        }
        lastPanelText = panelText;
        await page.wait({ time: 0.5 });
    }
    throw new CommandExecutionError('等待抖音封面处理完成超时', lastPanelText || 'unknown');
}
/**
 * Click the draft button on the composer page and extract the current creation id.
 */
async function clickSaveDraft(page) {
    const result = (await page.evaluate(`() => {
    const extractCreationId = () => {
      const titleInput = Array.from(document.querySelectorAll('input')).find(
        (el) => (el.placeholder || '').includes('填写作品标题')
      );
      if (!(titleInput instanceof HTMLInputElement)) return '';

      const fiberKey = Object.keys(titleInput).find((key) => key.startsWith('__reactFiber$'));
      let fiber = fiberKey ? titleInput[fiberKey] : null;
      while (fiber) {
        const props = fiber.memoizedProps;
        if (typeof props?.creation_id === 'string' && props.creation_id) {
          return props.creation_id;
        }
        fiber = fiber.return;
      }
      return '';
    };

    const btn = Array.from(document.querySelectorAll('button')).find(
      (el) => (el.textContent || '').includes('暂存离开')
    );
    if (!(btn instanceof HTMLButtonElement)) {
      return { ok: false, reason: 'draft-button-missing' };
    }
    const creationId = extractCreationId();
    const propKey = Object.keys(btn).find((key) => key.startsWith('__reactProps$'));
    const props = propKey ? btn[propKey] : null;
    if (props?.onClick) {
      props.onClick({
        preventDefault() {},
        stopPropagation() {},
        nativeEvent: null,
        target: btn,
        currentTarget: btn,
      });
    } else {
      btn.click();
    }
    return {
      ok: true,
      text: (btn.textContent || '').trim(),
      creationId,
    };
  }`));
    if (!result?.ok) {
        throw new CommandExecutionError(`点击草稿按钮失败: ${result?.reason || 'unknown'}`);
    }
    if (!result.creationId) {
        throw new CommandExecutionError('点击草稿按钮失败: creation-id-missing');
    }
    return {
        text: result.text || '暂存离开',
        creationId: result.creationId,
    };
}
/**
 * Wait until creator center shows the resumable-draft prompt after saving.
 */
async function waitForDraftResult(page, creationId) {
    let lastState = { href: '', bodyText: '' };
    for (let attempt = 0; attempt < 20; attempt += 1) {
        lastState = (await page.evaluate(`() => ({
      href: location.href,
      bodyText: document.body?.innerText || ''
    })`));
        if (lastState.href.includes('/creator-micro/content/upload')
            && /继续编辑/.test(lastState.bodyText)) {
            return creationId;
        }
        await page.wait({ time: 1 });
    }
    throw new CommandExecutionError('未检测到抖音草稿恢复提示', `当前页面: ${lastState.href || 'unknown'}`);
}
cli({
    site: 'douyin',
    name: 'draft',
    access: 'write',
    description: '上传视频并保存为草稿',
    domain: 'creator.douyin.com',
    strategy: Strategy.COOKIE,
    navigateBefore: false,
    args: [
        { name: 'video', required: true, positional: true, help: '视频文件路径' },
        { name: 'title', required: true, help: '视频标题（≤30字）' },
        { name: 'caption', default: '', help: '正文内容（≤1000字，支持 #话题）' },
        { name: 'cover', default: '', help: '封面图片路径' },
        { name: 'visibility', default: 'public', choices: ['public', 'friends', 'private'] },
    ],
    columns: ['status', 'draft_id'],
    func: async (page, kwargs) => {
        const videoPath = path.resolve(kwargs.video);
        if (!fs.existsSync(videoPath)) {
            throw new ArgumentError(`视频文件不存在: ${videoPath}`);
        }
        const ext = path.extname(videoPath).toLowerCase();
        if (!['.mp4', '.mov', '.avi', '.webm'].includes(ext)) {
            throw new ArgumentError(`不支持的视频格式: ${ext}（支持 mp4/mov/avi/webm）`);
        }
        const title = kwargs.title;
        if (title.length > 30) {
            throw new ArgumentError('标题不能超过 30 字');
        }
        const caption = kwargs.caption || '';
        if (caption.length > 1000) {
            throw new ArgumentError('正文不能超过 1000 字');
        }
        const coverPath = kwargs.cover;
        if (coverPath) {
            if (!fs.existsSync(path.resolve(coverPath))) {
                throw new ArgumentError(`封面文件不存在: ${path.resolve(coverPath)}`);
            }
        }
        if (!page.setFileInput) {
            throw new CommandExecutionError('当前浏览器适配器不支持文件注入', '请使用 Browser Bridge 或支持 setFileInput 的浏览器模式');
        }
        const visibilityLabel = VISIBILITY_LABELS[kwargs.visibility] ?? VISIBILITY_LABELS.public;
        await page.goto(DRAFT_UPLOAD_URL);
        await page.wait({ selector: 'input[type="file"]', timeout: 20 });
        await dismissKnownModals(page);
        await page.setFileInput([videoPath], 'input[type="file"]');
        await waitForDraftComposer(page);
        await dismissKnownModals(page);
        if (coverPath) {
            const coverSelector = await prepareCustomCoverInput(page);
            await page.setFileInput([path.resolve(coverPath)], coverSelector);
            await waitForCoverReady(page);
        }
        await fillDraftComposer(page, { title, caption, visibilityLabel });
        await page.wait({ time: 1 });
        const saveResult = await clickSaveDraft(page);
        const draftId = await waitForDraftResult(page, saveResult.creationId);
        return [
            {
                status: '✅ 草稿已保存，可在创作中心继续编辑',
                draft_id: draftId,
            },
        ];
    },
});
