import { Injectable } from '@nestjs/common';
import { mkdir, writeFile } from 'fs/promises';
import path from 'path';
import { chromium, Page } from 'playwright';
import { InspectionTaskEntity } from '../tasks/inspection-task.entity';
import {
  InspectionExecutionResult,
  InspectionStep,
  InspectionStepLog,
} from './inspector.types';

@Injectable()
export class InspectorService {
  async executeTask(
    task: Pick<InspectionTaskEntity, 'id' | 'name' | 'siteUrl' | 'stepsJson'>,
    traceId: string,
  ): Promise<InspectionExecutionResult> {
    const browser = await chromium.launch({ headless: true });
    const context = await browser.newContext({
      viewport: { width: 1440, height: 960 },
      ignoreHTTPSErrors: true,
    });
    const page = await context.newPage();

    const runDir = path.resolve(process.cwd(), 'storage', 'runs', traceId);
    const screenshotsDir = path.join(runDir, 'screenshots');
    await mkdir(screenshotsDir, { recursive: true });

    const steps = this.parseSteps(task.stepsJson);
    const stepLogs: InspectionStepLog[] = [];
    const screenshotPaths: string[] = [];
    let errorTag: string | null = null;
    let failureReason: string | null = null;
    let resultStatus: InspectionExecutionResult['status'] = 'success';

    try {
      for (const [index, step] of steps.entries()) {
        await this.executeStep({
          page,
          step,
          index,
          task,
          traceId,
          screenshotsDir,
          screenshotPaths,
          stepLogs,
        });
      }

      const domSummary = await this.readDomSummary(page);
      const blankPage = await this.detectBlankPage(page);

      if (blankPage) {
        errorTag = 'BLANK_PAGE';
        failureReason = '页面主体文本过少，命中白屏检测规则。';
        resultStatus = 'failed';
        await this.captureEvidence(
          page,
          screenshotsDir,
          traceId,
          task.id,
          screenshotPaths,
          'blank-page',
        );
      } else if (screenshotPaths.length === 0) {
        await this.captureEvidence(
          page,
          screenshotsDir,
          traceId,
          task.id,
          screenshotPaths,
          'final',
        );
      }

      const resultJsonUrl = await this.writeResultFile({
        runDir,
        traceId,
        task,
        status: resultStatus,
        errorTag,
        failureReason,
        screenshotPaths,
        stepLogs,
        pageUrl: page.url() || task.siteUrl,
        domSummary,
      });

      return {
        executor: 'browser',
        status: resultStatus,
        errorTag,
        failureReason,
        screenshotPaths,
        stepLogs,
        pageUrl: page.url() || task.siteUrl,
        domSummary,
        evidenceDir: `/runs/${traceId}`,
        resultJsonUrl,
      };
    } catch (error) {
      const message =
        error instanceof Error ? error.message : '执行器出现未知异常。';
      const domSummary = await this.safeDomSummary(page);

      errorTag = this.classifyError(message);
      failureReason = message;
      resultStatus = screenshotPaths.length > 0 ? 'partial' : 'failed';

      stepLogs.push({
        index: stepLogs.length + 1,
        action: 'screenshot',
        status: 'failed',
        message,
        timestamp: new Date().toISOString(),
      });

      await this.captureEvidence(
        page,
        screenshotsDir,
        traceId,
        task.id,
        screenshotPaths,
        'failed',
      );

      const resultJsonUrl = await this.writeResultFile({
        runDir,
        traceId,
        task,
        status: resultStatus,
        errorTag,
        failureReason,
        screenshotPaths,
        stepLogs,
        pageUrl: page.url() || task.siteUrl,
        domSummary,
      });

      return {
        executor: 'browser',
        status: resultStatus,
        errorTag,
        failureReason,
        screenshotPaths,
        stepLogs,
        pageUrl: page.url() || task.siteUrl,
        domSummary,
        evidenceDir: `/runs/${traceId}`,
        resultJsonUrl,
      };
    } finally {
      await page.close();
      await context.close();
      await browser.close();
    }
  }

  private parseSteps(stepsJson: string): InspectionStep[] {
    const parsed: unknown = JSON.parse(stepsJson);
    if (!Array.isArray(parsed) || parsed.length === 0) {
      throw new Error('steps_json 不能为空，且必须是非空数组。');
    }
    return parsed as InspectionStep[];
  }

  private async executeStep(args: {
    page: Page;
    step: InspectionStep;
    index: number;
    task: Pick<InspectionTaskEntity, 'id' | 'name' | 'siteUrl'>;
    traceId: string;
    screenshotsDir: string;
    screenshotPaths: string[];
    stepLogs: InspectionStepLog[];
  }) {
    const {
      page,
      step,
      index,
      task,
      traceId,
      screenshotsDir,
      screenshotPaths,
      stepLogs,
    } = args;
    const timeout = step.timeoutMs ?? 10000;

    switch (step.action) {
      case 'goto': {
        const targetUrl = step.url ?? task.siteUrl;
        const statusCode = await this.gotoWithRetry(page, targetUrl, timeout);
        if (statusCode >= 400) {
          throw new Error(`HTTP_${String(statusCode)}: 站点访问返回异常状态码。`);
        }
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `访问 ${targetUrl}`,
        );
        return;
      }
      case 'waitForSelector': {
        if (!step.selector) {
          throw new Error('ELEMENT_MISSING: waitForSelector 缺少 selector。');
        }
        await page.waitForSelector(step.selector, { timeout });
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `元素出现 ${step.selector}`,
        );
        return;
      }
      case 'waitForLoadState': {
        const state =
          step.value === 'load' || step.value === 'networkidle'
            ? step.value
            : 'domcontentloaded';
        await page.waitForLoadState(state, { timeout });
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `页面进入 ${state} 状态`,
        );
        return;
      }
      case 'click': {
        if (!step.selector) {
          throw new Error('ELEMENT_MISSING: click 缺少 selector。');
        }
        await page.click(step.selector, { timeout });
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `点击 ${step.selector}`,
        );
        return;
      }
      case 'input': {
        if (!step.selector) {
          throw new Error('ELEMENT_MISSING: input 缺少 selector。');
        }
        await page.fill(step.selector, step.value ?? '', { timeout });
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `输入 ${step.selector}`,
        );
        return;
      }
      case 'assertText': {
        if (!step.selector) {
          throw new Error('ELEMENT_MISSING: assertText 缺少 selector。');
        }
        const text = await page.textContent(step.selector, { timeout });
        if (!text || !text.includes(step.contains ?? '')) {
          throw new Error(
            `ELEMENT_MISSING: ${step.selector} 未包含断言文本。`,
          );
        }
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `文本断言通过 ${step.selector}`,
        );
        return;
      }
      case 'assertVisible': {
        if (!step.selector) {
          throw new Error('ELEMENT_MISSING: assertVisible 缺少 selector。');
        }
        await page.locator(step.selector).waitFor({
          state: 'visible',
          timeout,
        });
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `可见性断言通过 ${step.selector}`,
        );
        return;
      }
      case 'assertUrlIncludes': {
        const contains = step.contains ?? step.value;
        if (!contains) {
          throw new Error('EXECUTION_ERROR: assertUrlIncludes 缺少 contains。');
        }
        const currentUrl = page.url();
        if (!currentUrl.includes(contains)) {
          throw new Error(`EXECUTION_ERROR: 当前 URL 未包含 ${contains}。`);
        }
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `URL 断言通过，命中 ${contains}`,
        );
        return;
      }
      case 'sleep': {
        await page.waitForTimeout(timeout);
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `等待 ${String(timeout)}ms`,
        );
        return;
      }
      case 'screenshot': {
        const label = step.value?.trim() || `step-${String(index + 1)}`;
        await this.captureEvidence(
          page,
          screenshotsDir,
          traceId,
          task.id,
          screenshotPaths,
          label,
        );
        this.pushStepLog(
          stepLogs,
          index,
          step.action,
          'success',
          `已保存截图 ${label}`,
        );
        return;
      }
      default:
        throw new Error(`EXECUTION_ERROR: 不支持的步骤类型 ${String(step.action)}。`);
    }
  }

  private async gotoWithRetry(page: Page, targetUrl: string, timeout: number) {
    let lastError: unknown = null;

    for (const waitUntil of ['domcontentloaded', 'networkidle'] as const) {
      try {
        const response = await page.goto(targetUrl, {
          waitUntil,
          timeout,
        });
        return response?.status() ?? 0;
      } catch (error) {
        lastError = error;
      }
    }

    throw lastError instanceof Error
      ? lastError
      : new Error('EXECUTION_ERROR: 页面导航失败。');
  }

  private pushStepLog(
    stepLogs: InspectionStepLog[],
    index: number,
    action: InspectionStep['action'],
    status: 'success' | 'failed',
    message: string,
  ) {
    stepLogs.push({
      index: index + 1,
      action,
      status,
      message,
      timestamp: new Date().toISOString(),
    });
  }

  private async captureEvidence(
    page: Page,
    screenshotsDir: string,
    traceId: string,
    taskId: string,
    screenshotPaths: string[],
    label: string,
  ) {
    await page.evaluate(
      (payload: { traceId: string; taskId: string }) => {
        const existing = document.getElementById('sentai-watermark');
        if (existing) {
          existing.remove();
        }

        const watermark = document.createElement('div');
        watermark.id = 'sentai-watermark';
        watermark.style.position = 'fixed';
        watermark.style.right = '16px';
        watermark.style.bottom = '16px';
        watermark.style.padding = '8px 12px';
        watermark.style.background = 'rgba(15, 23, 42, 0.75)';
        watermark.style.color = '#fff';
        watermark.style.fontSize = '12px';
        watermark.style.borderRadius = '12px';
        watermark.style.zIndex = '2147483647';
        watermark.style.pointerEvents = 'none';
        watermark.textContent = `${payload.taskId} | ${payload.traceId} | ${new Date().toISOString()}`;
        document.body.appendChild(watermark);
      },
      { traceId, taskId },
    );

    const safeLabel = label.replace(/[^a-zA-Z0-9-_]/g, '-');
    const filename = `${safeLabel}-${Date.now()}.png`;
    const outputPath = path.join(screenshotsDir, filename);
    await page.screenshot({ path: outputPath, fullPage: true });
    screenshotPaths.push(`/runs/${traceId}/screenshots/${filename}`);
  }

  private async readDomSummary(page: Page) {
    return page.evaluate(() => {
      const title = document.title || '无标题';
      const bodyText =
        document.body?.innerText?.replace(/\s+/g, ' ').trim() ?? '';
      return `${title} | ${bodyText.slice(0, 500)}`;
    });
  }

  private async safeDomSummary(page: Page) {
    try {
      return await this.readDomSummary(page);
    } catch {
      return null;
    }
  }

  private async detectBlankPage(page: Page) {
    return page.evaluate(() => {
      const text = document.body?.innerText?.replace(/\s+/g, '').trim() ?? '';
      const hasVisualBlock = Boolean(
        document.querySelector('img, main, article, section, header, footer'),
      );
      return text.length < 20 && !hasVisualBlock;
    });
  }

  private classifyError(message: string) {
    if (message.includes('HTTP_')) {
      return message.slice(0, message.indexOf(':'));
    }
    if (message.includes('ELEMENT_MISSING')) {
      return 'ELEMENT_MISSING';
    }
    if (message.includes('Timeout')) {
      return 'TIMEOUT';
    }
    return 'EXECUTION_ERROR';
  }

  private async writeResultFile(args: {
    runDir: string;
    traceId: string;
    task: Pick<InspectionTaskEntity, 'id' | 'name' | 'siteUrl'>;
    status: InspectionExecutionResult['status'];
    errorTag: string | null;
    failureReason: string | null;
    screenshotPaths: string[];
    stepLogs: InspectionStepLog[];
    pageUrl: string | null;
    domSummary: string | null;
  }) {
    const resultPath = path.join(args.runDir, 'result.json');
    await writeFile(
      resultPath,
      JSON.stringify(
        {
          runId: args.traceId,
          executor: 'browser',
          task: args.task,
          status: args.status,
          errorTag: args.errorTag,
          failureReason: args.failureReason,
          pageUrl: args.pageUrl,
          domSummary: args.domSummary,
          screenshots: args.screenshotPaths,
          stepLogs: args.stepLogs,
          generatedAt: new Date().toISOString(),
        },
        null,
        2,
      ),
      'utf8',
    );

    return `/runs/${args.traceId}/result.json`;
  }
}
