import fs from 'node:fs/promises';
import path from 'node:path';
import pg from 'pg';

const DEFAULT_DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:54322/postgres';
const DEFAULT_TENANT_ID = '00000000-0000-0000-0000-000000000001';
const DATABASE_URL = process.env.DATABASE_URL || DEFAULT_DATABASE_URL;
const TENANT_ID = process.env.PB_SAMPLE_TENANT_ID || process.env.TENANT_ID || DEFAULT_TENANT_ID;
const TENANT_SLUG = process.env.PB_SAMPLE_TENANT_SLUG || '';
const OUTPUT_DIR = process.env.PB_SAMPLE_OUTPUT_DIR || 'docs/refactor/migration-reports';
const WRITE_REPORT = envBool('PB_SAMPLE_WRITE_REPORT', false);
const FAIL_ON_WARNINGS = envBool('PB_SAMPLE_FAIL_ON_WARNINGS', false);
const SAMPLE_LIMIT = envNumber('PB_SAMPLE_LIMIT', 5);

const ALLOWED_CRITICAL_IMPORT_ISSUES = new Set([
  'orders:order_user_not_resolved',
  'handbook_chapters:handbook_chapter_subject_missing',
]);

const TABLES = [
  'tenant_memberships',
  'content_entries',
  'content_nodes',
  'question_collections',
  'question_collection_items',
  'practice_blueprints',
  'questions',
  'question_versions',
  'answer_records',
  'wrong_questions',
  'favorite_questions',
  'vocabulary_units',
  'vocabulary_words',
  'handbook_subjects',
  'handbook_chapters',
  'handbook_entries',
  'scoreline_fields',
  'scoreline_schools',
  'scoreline_majors',
  'scoreline_records',
  'orders',
  'payments',
  'entitlements',
  'activation_codes',
  'content_assets',
  'video_explanations',
  'question_videos',
  'pb_raw_records',
  'pb_import_issues',
];

function envBool(key, fallback) {
  const value = process.env[key];
  if (value === undefined || value === '') return fallback;
  return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
}

function envNumber(key, fallback) {
  const raw = process.env[key];
  if (!raw) return fallback;
  const value = Number(raw);
  return Number.isFinite(value) && value > 0 ? value : fallback;
}

function intValue(value) {
  return Number(value || 0);
}

function shanghaiTimestampForFile(date = new Date()) {
  const parts = Object.fromEntries(
    new Intl.DateTimeFormat('en-CA', {
      timeZone: 'Asia/Shanghai',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false,
    }).formatToParts(date).map(part => [part.type, part.value]),
  );
  return `${parts.year}${parts.month}${parts.day}-${parts.hour}${parts.minute}${parts.second}`;
}

class BusinessSampler {
  constructor(client) {
    this.client = client;
    this.checks = [];
    this.samples = {};
    this.counts = {};
    this.rawCounts = {};
    this.tenant = null;
    this.latestRun = null;
  }

  add(status, name, message, details = {}) {
    this.checks.push({
      status,
      name,
      message,
      ...details,
    });
  }

  pass(name, message, details = {}) {
    this.add('pass', name, message, details);
  }

  warn(name, message, details = {}) {
    this.add('warn', name, message, details);
  }

  fail(name, message, details = {}) {
    this.add('fail', name, message, details);
  }

  skip(name, message, details = {}) {
    this.add('skip', name, message, details);
  }

  async one(sql, params = []) {
    const result = await this.client.query(sql, params);
    return result.rows[0] || null;
  }

  async many(sql, params = []) {
    const result = await this.client.query(sql, params);
    return result.rows;
  }

  async scalar(sql, params = []) {
    const row = await this.one(sql, params);
    if (!row) return 0;
    return intValue(row.count ?? row.value ?? Object.values(row)[0]);
  }

  async run() {
    await this.discoverTenant();
    await this.captureCounts();
    await this.checkTenantAndImportRun();
    await this.checkTenantIsolationInvariants();
    await this.checkPracticeNavigation();
    await this.checkLearningRecords();
    await this.checkVocabulary();
    await this.checkHandbook();
    await this.checkScoreline();
    await this.checkCommerce();
    await this.checkAssetsAndVideos();
    await this.checkImportIssues();
    await this.checkSensitiveDataLeaks();
    return this.buildReport();
  }

  async discoverTenant() {
    const tenant = TENANT_SLUG
      ? await this.one(
          `
          select id, slug::text, name, status, mode, billing_status as "billingStatus"
          from public.tenants
          where slug = $1
          limit 1
          `,
          [TENANT_SLUG],
        )
      : await this.one(
          `
          select id, slug::text, name, status, mode, billing_status as "billingStatus"
          from public.tenants
          where id = $1
          limit 1
          `,
          [TENANT_ID],
        );

    if (!tenant) {
      const hint = TENANT_SLUG ? `slug=${TENANT_SLUG}` : `id=${TENANT_ID}`;
      throw new Error(`Tenant not found for ${hint}. Run PocketBase import or set PB_SAMPLE_TENANT_ID/PB_SAMPLE_TENANT_SLUG.`);
    }

    this.tenant = tenant;
    this.latestRun = await this.one(
      `
      select id, source_name as "sourceName", source_kind as "sourceKind", status,
             started_at as "startedAt", finished_at as "finishedAt"
      from public.pb_import_runs
      where tenant_id = $1
      order by started_at desc
      limit 1
      `,
      [tenant.id],
    );
  }

  async captureCounts() {
    for (const table of TABLES) {
      const tableRef = `public.${table}`;
      if (table === 'pb_raw_records') {
        this.counts[table] = await this.scalar(`select count(*) from ${tableRef} where tenant_id = $1`, [this.tenant.id]);
      } else if (table === 'tenant_memberships') {
        this.counts[table] = await this.scalar(`select count(*) from ${tableRef} where tenant_id = $1`, [this.tenant.id]);
      } else {
        this.counts[table] = await this.scalar(`select count(*) from ${tableRef} where tenant_id = $1`, [this.tenant.id]);
      }
    }

    if (!this.latestRun) return;
    const rows = await this.many(
      `
      select collection_name as "collectionName", count(*)::integer as count
      from public.pb_raw_records
      where tenant_id = $1 and run_id = $2
      group by collection_name
      order by collection_name asc
      `,
      [this.tenant.id, this.latestRun.id],
    );
    this.rawCounts = Object.fromEntries(rows.map(row => [row.collectionName, intValue(row.count)]));
  }

  rawCount(...collectionNames) {
    return collectionNames.reduce((sum, name) => sum + intValue(this.rawCounts[name]), 0);
  }

  hasRaw(...collectionNames) {
    return this.rawCount(...collectionNames) > 0;
  }

  async checkTenantAndImportRun() {
    if (this.tenant.status === 'active') {
      this.pass('tenant.active', `租户 ${this.tenant.slug} 是 active 状态。`);
    } else {
      this.warn('tenant.not_active', `租户 ${this.tenant.slug} 当前状态为 ${this.tenant.status}，联调前需确认是否允许前端访问。`, {
        tenantStatus: this.tenant.status,
      });
    }

    if (!this.latestRun) {
      this.fail('import.latest_run_missing', '未找到 PocketBase 导入 run，无法证明真实旧数据已经导入。');
      return;
    }
    if (this.latestRun.status === 'completed') {
      this.pass('import.latest_run_completed', '最新 PocketBase 导入 run 已完成。', {
        runId: this.latestRun.id,
        sourceName: this.latestRun.sourceName,
      });
    } else {
      this.fail('import.latest_run_not_completed', `最新 PocketBase 导入 run 状态为 ${this.latestRun.status}。`, {
        runId: this.latestRun.id,
      });
    }

    const activeStudent = await this.one(
      `
      select pu.id
      from public.tenant_memberships tm
      join public.platform_users pu on pu.id = tm.user_id
      where tm.tenant_id = $1
        and tm.role = 'student'
        and tm.status = 'active'
        and coalesce(pu.status, 'active') = 'active'
      order by pu.created_at asc
      limit 1
      `,
      [this.tenant.id],
    );
    if (activeStudent) {
      this.samples.studentId = activeStudent.id;
      this.pass('identity.active_student_exists', '存在可用于学生端联调的 active 学生账号。');
    } else {
      this.fail('identity.active_student_missing', '未找到 active 学生账号，学生端登录后无法抽样验证业务链路。');
    }

    const adminMember = await this.one(
      `
      select tm.user_id as "userId", tm.role
      from public.tenant_memberships tm
      where tm.tenant_id = $1
        and tm.status = 'active'
        and tm.role in ('tenant_owner', 'tenant_admin', 'tenant_operator')
      order by case tm.role when 'tenant_owner' then 0 when 'tenant_admin' then 1 else 2 end
      limit 1
      `,
      [this.tenant.id],
    );
    if (adminMember) {
      this.samples.tenantAdminUserId = adminMember.userId;
      this.pass('identity.tenant_admin_exists', '存在可用于租户后台联调的 active 管理成员。', { role: adminMember.role });
    } else {
      this.warn('identity.tenant_admin_missing', '未找到 active 租户后台成员，后续需要创建管理员账号再做后台联调。');
    }
  }

  async checkTenantIsolationInvariants() {
    const nodeEntryMismatch = await this.scalar(
      `
      select count(*)
      from public.content_nodes cn
      join public.content_entries ce on ce.id = cn.entry_id
      where cn.tenant_id = $1 and ce.tenant_id <> cn.tenant_id
      `,
      [this.tenant.id],
    );
    nodeEntryMismatch === 0
      ? this.pass('isolation.content_node_entry_tenant_match', '内容节点与入口 tenant_id 一致。')
      : this.fail('isolation.content_node_entry_tenant_mismatch', '存在内容节点引用其它租户入口。', { count: nodeEntryMismatch });

    const collectionItemMismatch = await this.scalar(
      `
      select count(*)
      from public.question_collection_items qci
      join public.question_collections qc on qc.id = qci.collection_id
      join public.questions q on q.id = qci.question_id
      where qci.tenant_id = $1
        and (qc.tenant_id <> qci.tenant_id or q.tenant_id <> qci.tenant_id)
      `,
      [this.tenant.id],
    );
    collectionItemMismatch === 0
      ? this.pass('isolation.collection_item_tenant_match', '题目合集条目与合集/题目 tenant_id 一致。')
      : this.fail('isolation.collection_item_tenant_mismatch', '存在题目合集条目跨租户引用。', { count: collectionItemMismatch });

    const learningMismatch = await this.scalar(
      `
      select count(*)
      from (
        select fq.tenant_id, fq.question_id from public.favorite_questions fq where fq.tenant_id = $1
        union all
        select wq.tenant_id, wq.question_id from public.wrong_questions wq where wq.tenant_id = $1
      ) refs
      left join public.questions q on q.id = refs.question_id and q.tenant_id = refs.tenant_id
      where q.id is null
      `,
      [this.tenant.id],
    );
    learningMismatch === 0
      ? this.pass('isolation.learning_question_refs_valid', '错题/收藏均引用本租户有效题目。')
      : this.fail('isolation.learning_question_refs_invalid', '错题或收藏存在无效/跨租户题目引用。', { count: learningMismatch });

    const handbookMismatch = await this.scalar(
      `
      select count(*)
      from public.handbook_chapters hc
      join public.handbook_subjects hs on hs.id = hc.subject_id
      where hc.tenant_id = $1 and hs.tenant_id <> hc.tenant_id
      `,
      [this.tenant.id],
    );
    handbookMismatch === 0
      ? this.pass('isolation.handbook_refs_valid', '知识手册章节与手册科目 tenant_id 一致。')
      : this.fail('isolation.handbook_refs_invalid', '知识手册章节存在跨租户科目引用。', { count: handbookMismatch });

    const vocabularyMismatch = await this.scalar(
      `
      select count(*)
      from public.vocabulary_words vw
      join public.vocabulary_units vu on vu.id = vw.unit_id
      where vw.tenant_id = $1 and vu.tenant_id <> vw.tenant_id
      `,
      [this.tenant.id],
    );
    vocabularyMismatch === 0
      ? this.pass('isolation.vocabulary_refs_valid', '单词与单元 tenant_id 一致。')
      : this.fail('isolation.vocabulary_refs_invalid', '单词存在跨租户单元引用。', { count: vocabularyMismatch });
  }

  async checkPracticeNavigation() {
    const legacyQuestionCount = this.counts.questions;
    if (legacyQuestionCount === 0) {
      this.fail('practice.questions_missing', '当前租户没有题目，无法支撑刷题业务。');
      return;
    }

    const entries = await this.many(
      `
      select id, name, entry_type as "entryType", visibility
      from public.content_entries
      where tenant_id = $1
        and entry_type = 'question_practice'
        and is_active = true
        and visibility <> 'hidden'
      order by sort_order asc, created_at asc
      limit $2
      `,
      [this.tenant.id, SAMPLE_LIMIT],
    );
    if (entries.length) {
      this.samples.practiceEntryId = entries[0].id;
      this.pass('practice.entries_available', `存在 ${entries.length} 个可见题库入口样本。`, { sampleCount: entries.length });
    } else {
      this.fail('practice.entries_missing', '没有 active 且可见的 question_practice 入口，学生端首页无法进入题库。');
    }

    const nodeStats = await this.one(
      `
      select count(*)::integer as count, coalesce(max(depth), 0)::integer as "maxDepth"
      from public.content_nodes
      where tenant_id = $1 and is_active = true
      `,
      [this.tenant.id],
    );
    if (intValue(nodeStats?.count) > 0) {
      this.pass('practice.nodes_available', `存在 ${nodeStats.count} 个 active 内容节点，最大深度 ${nodeStats.maxDepth}。`, {
        count: intValue(nodeStats.count),
        maxDepth: intValue(nodeStats.maxDepth),
      });
    } else {
      this.fail('practice.nodes_missing', '没有 active 内容节点，任意深度分类树不可用。');
    }

    const collections = await this.many(
      `
      select qc.id, qc.name, qc.question_count as "questionCount",
             count(qci.id)::integer as "itemCount",
             count(q.id)::integer as "resolvedQuestions",
             count(qv.id)::integer as "resolvedVersions",
             count(nullif(qv.content, ''))::integer as "contentQuestions"
      from public.question_collections qc
      left join public.question_collection_items qci
        on qci.tenant_id = qc.tenant_id and qci.collection_id = qc.id
      left join public.questions q
        on q.tenant_id = qc.tenant_id and q.id = qci.question_id and q.status = 'published'
      left join public.question_versions qv
        on qv.tenant_id = q.tenant_id and qv.id = q.current_version_id
      where qc.tenant_id = $1 and qc.status = 'active'
      group by qc.id, qc.name, qc.question_count
      having count(qci.id) > 0
      order by count(qci.id) desc, qc.created_at asc
      limit $2
      `,
      [this.tenant.id, SAMPLE_LIMIT],
    );
    if (collections.length) {
      this.samples.collectionId = collections[0].id;
      const usable = collections.some(item => intValue(item.resolvedQuestions) > 0 && intValue(item.contentQuestions) > 0);
      usable
        ? this.pass('practice.collections_usable', '题目合集可以解析到已发布题目和当前题目版本。', { sampleCount: collections.length })
        : this.fail('practice.collections_without_question_content', '题目合集有条目，但无法解析到带内容的当前题目版本。', { sampleCount: collections.length });
    } else {
      this.fail('practice.collections_missing', '没有含题目的 active 题目合集，顺序/随机刷题无法组卷。');
    }

    const blueprintRows = await this.many(
      `
      select mode, count(*)::integer as count
      from public.practice_blueprints
      where tenant_id = $1 and status = 'active'
      group by mode
      order by mode asc
      `,
      [this.tenant.id],
    );
    const blueprintCounts = Object.fromEntries(blueprintRows.map(row => [row.mode, intValue(row.count)]));
    if (blueprintCounts.sequential > 0 && blueprintCounts.random > 0) {
      this.pass('practice.blueprints_core_modes', '顺序刷题和随机刷题蓝图均可用。', blueprintCounts);
    } else {
      this.fail('practice.blueprints_core_modes_missing', '缺少顺序刷题或随机刷题蓝图。', blueprintCounts);
    }
    if (blueprintCounts.mock_exam > 0) {
      this.pass('practice.blueprints_mock_exam', '存在全真模拟蓝图。', { count: blueprintCounts.mock_exam });
    } else {
      this.warn('practice.blueprints_mock_exam_missing', '未发现全真模拟蓝图；如果旧库没有 mock_exam_configs 可忽略，否则需补导入。');
    }

    const blueprintBrokenRefs = await this.scalar(
      `
      select count(*)
      from public.practice_blueprints pb
      left join public.question_collections qc
        on qc.tenant_id = pb.tenant_id and qc.id = pb.collection_id
      where pb.tenant_id = $1
        and pb.status = 'active'
        and pb.assembly_type = 'collection'
        and (pb.collection_id is null or qc.id is null or qc.status <> 'active' or qc.question_count = 0)
      `,
      [this.tenant.id],
    );
    blueprintBrokenRefs === 0
      ? this.pass('practice.blueprints_collection_refs_valid', 'active 练习蓝图均指向可用合集。')
      : this.fail('practice.blueprints_collection_refs_invalid', '存在 active 练习蓝图无法指向可用合集。', { count: blueprintBrokenRefs });

    const questionsWithoutNavigation = await this.scalar(
      `
      select count(*)
      from public.questions
      where tenant_id = $1
        and legacy_id is not null
        and status = 'published'
        and (entry_id is null or content_node_id is null or primary_collection_id is null)
      `,
      [this.tenant.id],
    );
    questionsWithoutNavigation === 0
      ? this.pass('practice.legacy_questions_navigation_complete', '已发布旧题均已挂接入口、分类节点和主合集。')
      : this.fail('practice.legacy_questions_navigation_incomplete', '存在已发布旧题未挂接新题库导航。', { count: questionsWithoutNavigation });

    const hiddenReviewQuestions = await this.scalar(
      `
      select count(*)
      from public.questions q
      join public.question_collections qc
        on qc.tenant_id = q.tenant_id and qc.id = q.primary_collection_id
      where q.tenant_id = $1
        and qc.status = 'draft'
        and qc.access_rules->>'requiresReview' = 'true'
      `,
      [this.tenant.id],
    );
    if (hiddenReviewQuestions > 0) {
      this.warn('practice.migration_review_questions', '有旧题被隔离到迁移待复核合集，正式上线前需要运营确认。', {
        count: hiddenReviewQuestions,
      });
    } else {
      this.pass('practice.no_migration_review_questions', '没有题目停留在迁移待复核合集。');
    }

    const sampleQuestions = await this.many(
      `
      select q.id, q.type, q.type_label as "typeLabel",
             q.entry_id as "entryId", q.content_node_id as "contentNodeId", q.primary_collection_id as "collectionId",
             length(coalesce(qv.content, '')) as "contentLength",
             jsonb_array_length(coalesce(qv.options, '[]'::jsonb)) as "optionCount",
             jsonb_array_length(coalesce(qv.sub_questions, '[]'::jsonb)) as "subQuestionCount",
             q.has_video_explanation as "hasVideoExplanation"
      from public.questions q
      join public.question_versions qv on qv.id = q.current_version_id and qv.tenant_id = q.tenant_id
      where q.tenant_id = $1
        and q.status = 'published'
        and q.entry_id is not null
        and q.content_node_id is not null
        and q.primary_collection_id is not null
      order by q.created_at asc
      limit $2
      `,
      [this.tenant.id, SAMPLE_LIMIT],
    );
    this.samples.questions = sampleQuestions;
    if (sampleQuestions.length) {
      this.pass('practice.sample_questions_queryable', '学生端题目列表可抽样查询到题干、选项/子题和导航字段。', {
        sampleCount: sampleQuestions.length,
      });
    } else {
      this.fail('practice.sample_questions_missing', '无法抽样查询到可用于前端展示的已发布题目。');
    }
  }

  async checkLearningRecords() {
    const answerCount = this.counts.answer_records;
    if (answerCount > 0) {
      const resolvedAnswerCount = await this.scalar(
        `
        select count(*)
        from public.answer_records ar
        join public.questions q on q.id = ar.question_id and q.tenant_id = ar.tenant_id
        join public.platform_users pu on pu.id = ar.user_id
        where ar.tenant_id = $1
        `,
        [this.tenant.id],
      );
      resolvedAnswerCount > 0
        ? this.pass('learning.answer_records_queryable', '旧答题记录可解析到新用户和新题目。', {
            resolvedCount: resolvedAnswerCount,
            totalCount: answerCount,
          })
        : this.warn('learning.answer_records_not_queryable', '存在答题记录，但没有任何记录可同时解析到用户和题目。', {
            totalCount: answerCount,
          });

      const unresolvedAnswers = await this.scalar(
        `
        select count(*)
        from public.answer_records
        where tenant_id = $1
          and legacy_question_id is not null
          and question_id is null
        `,
        [this.tenant.id],
      );
      if (unresolvedAnswers > 0) {
        this.warn('learning.answer_records_unresolved_questions', '部分旧答题记录引用已删除或未迁移题目，个人历史中应隐藏或标记不可复盘。', {
          count: unresolvedAnswers,
        });
      } else {
        this.pass('learning.answer_records_question_refs_complete', '旧答题记录题目引用均已解析。');
      }
    } else {
      this.warn('learning.answer_records_empty', '没有旧答题记录，学习统计只能从新系统开始累计。');
    }

    const wrongCount = this.counts.wrong_questions;
    wrongCount > 0
      ? this.pass('learning.wrong_questions_available', '错题本有可迁移数据。', { count: wrongCount })
      : this.warn('learning.wrong_questions_empty', '错题本没有迁移数据。');

    const favoriteCount = this.counts.favorite_questions;
    favoriteCount > 0
      ? this.pass('learning.favorite_questions_available', '收藏夹有可迁移数据。', { count: favoriteCount })
      : this.warn('learning.favorite_questions_empty', '收藏夹没有迁移数据。');

    const sampleWrong = await this.many(
      `
      select wq.question_id as "questionId", wq.wrong_count as "wrongCount"
      from public.wrong_questions wq
      join public.questions q on q.id = wq.question_id and q.tenant_id = wq.tenant_id
      where wq.tenant_id = $1
      order by wq.last_wrong_at desc
      limit $2
      `,
      [this.tenant.id, SAMPLE_LIMIT],
    );
    this.samples.wrongQuestions = sampleWrong;

    const sampleFavorite = await this.many(
      `
      select fq.question_id as "questionId"
      from public.favorite_questions fq
      join public.questions q on q.id = fq.question_id and q.tenant_id = fq.tenant_id
      where fq.tenant_id = $1
      order by fq.created_at desc
      limit $2
      `,
      [this.tenant.id, SAMPLE_LIMIT],
    );
    this.samples.favoriteQuestions = sampleFavorite;
  }

  async checkVocabulary() {
    const expected = this.hasRaw('vocabulary', 'vocabulary_words', 'vocabulary_units') || this.counts.vocabulary_words > 0;
    if (!expected) {
      this.skip('vocabulary.not_present', '旧库或当前租户没有单词数据。');
      return;
    }

    if (this.counts.vocabulary_units > 0 && this.counts.vocabulary_words > 0) {
      this.pass('vocabulary.units_words_available', '单词单元和单词均已落库。', {
        units: this.counts.vocabulary_units,
        words: this.counts.vocabulary_words,
      });
    } else {
      this.fail('vocabulary.units_or_words_missing', '旧库有单词数据，但新系统缺少单词单元或单词。', {
        units: this.counts.vocabulary_units,
        words: this.counts.vocabulary_words,
      });
    }

    const orphanWords = await this.scalar(
      `
      select count(*)
      from public.vocabulary_words vw
      left join public.vocabulary_units vu
        on vu.tenant_id = vw.tenant_id and vu.id = vw.unit_id
      where vw.tenant_id = $1 and vw.is_active = true and vu.id is null
      `,
      [this.tenant.id],
    );
    orphanWords === 0
      ? this.pass('vocabulary.words_have_units', 'active 单词均挂接到单词单元。')
      : this.fail('vocabulary.words_without_units', '存在 active 单词没有可用单元。', { count: orphanWords });

    const sampleWords = await this.many(
      `
      select vw.id, vw.unit_id as "unitId", length(vw.word) as "wordLength",
             length(coalesce(vw.meaning, '')) as "meaningLength",
             vw.difficulty
      from public.vocabulary_words vw
      where vw.tenant_id = $1 and vw.is_active = true
      order by vw.sort_order asc, vw.created_at asc
      limit $2
      `,
      [this.tenant.id, SAMPLE_LIMIT],
    );
    this.samples.vocabularyWords = sampleWords;
    sampleWords.some(word => intValue(word.wordLength) > 0 && intValue(word.meaningLength) > 0)
      ? this.pass('vocabulary.sample_words_queryable', '学生端背单词可抽样查询到单词和释义。', { sampleCount: sampleWords.length })
      : this.fail('vocabulary.sample_words_missing_content', '单词样本缺少单词或释义，前端无法正常展示。', { sampleCount: sampleWords.length });
  }

  async checkHandbook() {
    const expected = this.hasRaw('handbook_subjects', 'handbook_chapters', 'handbook_entries') || this.counts.handbook_entries > 0;
    if (!expected) {
      this.skip('handbook.not_present', '旧库或当前租户没有知识手册数据。');
      return;
    }

    if (this.counts.handbook_subjects > 0 && this.counts.handbook_chapters > 0 && this.counts.handbook_entries > 0) {
      this.pass('handbook.subjects_chapters_entries_available', '知识手册科目、章节和条目均已落库。', {
        subjects: this.counts.handbook_subjects,
        chapters: this.counts.handbook_chapters,
        entries: this.counts.handbook_entries,
      });
    } else {
      this.fail('handbook.structure_missing', '旧库有知识手册数据，但新系统手册科目/章节/条目不完整。', {
        subjects: this.counts.handbook_subjects,
        chapters: this.counts.handbook_chapters,
        entries: this.counts.handbook_entries,
      });
    }

    const orphanEntries = await this.scalar(
      `
      select count(*)
      from public.handbook_entries he
      left join public.handbook_chapters hc
        on hc.tenant_id = he.tenant_id and hc.id = he.chapter_id
      left join public.handbook_subjects hs
        on hs.tenant_id = hc.tenant_id and hs.id = hc.subject_id
      where he.tenant_id = $1
        and he.is_active = true
        and (hc.id is null or hs.id is null)
      `,
      [this.tenant.id],
    );
    if (orphanEntries > 0) {
      this.warn('handbook.entries_without_public_subject', '部分手册条目缺少可公开导航的章节/科目，已进入迁移复核或需人工归类。', {
        count: orphanEntries,
      });
    } else {
      this.pass('handbook.entries_have_navigation', 'active 手册条目均可通过科目/章节导航访问。');
    }

    const sampleEntries = await this.many(
      `
      select he.id, he.chapter_id as "chapterId", length(he.title) as "titleLength",
             length(coalesce(he.content, '')) as "contentLength"
      from public.handbook_entries he
      join public.handbook_chapters hc on hc.id = he.chapter_id and hc.tenant_id = he.tenant_id
      join public.handbook_subjects hs on hs.id = hc.subject_id and hs.tenant_id = he.tenant_id
      where he.tenant_id = $1 and he.is_active = true
      order by he.sort_order asc, he.created_at asc
      limit $2
      `,
      [this.tenant.id, SAMPLE_LIMIT],
    );
    this.samples.handbookEntries = sampleEntries;
    sampleEntries.some(entry => intValue(entry.titleLength) > 0 && intValue(entry.contentLength) > 0)
      ? this.pass('handbook.sample_entries_queryable', '学生端知识手册可抽样查询到标题和内容。', { sampleCount: sampleEntries.length })
      : this.fail('handbook.sample_entries_missing_content', '手册样本缺少标题或正文，前端无法正常展示。', { sampleCount: sampleEntries.length });
  }

  async checkScoreline() {
    const expected = this.hasRaw('scoreline_fields', 'scoreline_schools', 'scoreline_majors', 'scoreline_records') || this.counts.scoreline_records > 0;
    if (!expected) {
      this.skip('scoreline.not_present', '旧库或当前租户没有分数线数据。');
      return;
    }

    if (this.counts.scoreline_fields > 0 && this.counts.scoreline_records > 0) {
      this.pass('scoreline.fields_records_available', '分数线动态字段和记录均已落库。', {
        fields: this.counts.scoreline_fields,
        records: this.counts.scoreline_records,
      });
    } else {
      this.fail('scoreline.fields_or_records_missing', '旧库有分数线数据，但新系统缺少动态字段或记录。', {
        fields: this.counts.scoreline_fields,
        records: this.counts.scoreline_records,
      });
    }

    const recordWithFieldValues = await this.scalar(
      `
      select count(*)
      from public.scoreline_records
      where tenant_id = $1 and jsonb_typeof(field_values) = 'object' and field_values <> '{}'::jsonb
      `,
      [this.tenant.id],
    );
    recordWithFieldValues > 0
      ? this.pass('scoreline.records_have_dynamic_values', '分数线记录包含动态字段值，可支撑地区差异化展示。', { count: recordWithFieldValues })
      : this.warn('scoreline.records_without_dynamic_values', '分数线记录未发现动态字段值，前端只能展示院校/专业/年份基础信息。');

    const trendFields = await this.scalar(
      `
      select count(*)
      from public.scoreline_fields
      where tenant_id = $1 and is_trend = true and is_visible = true
      `,
      [this.tenant.id],
    );
    trendFields > 0
      ? this.pass('scoreline.trend_fields_available', '存在可用于趋势图的分数线字段。', { count: trendFields })
      : this.warn('scoreline.trend_fields_missing', '没有标记趋势字段，分数线趋势图需要前端选择默认数值字段或后台补配置。');

    const sampleRecords = await this.many(
      `
      select id, year, school_id as "schoolId", major_id as "majorId",
             (
               select count(*)::integer
               from jsonb_object_keys(field_values)
             ) as "fieldCount"
      from public.scoreline_records
      where tenant_id = $1
      order by year desc, created_at asc
      limit $2
      `,
      [this.tenant.id, SAMPLE_LIMIT],
    );
    this.samples.scorelineRecords = sampleRecords;
  }

  async checkCommerce() {
    if (this.counts.orders === 0) {
      this.warn('commerce.orders_empty', '没有订单迁移数据；如果旧库确实没有订单可忽略。');
      return;
    }

    this.pass('commerce.orders_available', '订单数据已迁移。', { count: this.counts.orders });

    const paidOrders = await this.scalar(
      `
      select count(*)
      from public.orders
      where tenant_id = $1 and status = 'paid'
      `,
      [this.tenant.id],
    );
    const paidWithoutPayment = await this.scalar(
      `
      select count(*)
      from public.orders o
      where o.tenant_id = $1
        and o.status = 'paid'
        and not exists (
          select 1 from public.payments p
          where p.tenant_id = o.tenant_id and p.order_id = o.id
        )
      `,
      [this.tenant.id],
    );
    paidWithoutPayment === 0
      ? this.pass('commerce.paid_orders_have_payment_rows', '已支付订单均有 payment 台账。', { paidOrders })
      : this.fail('commerce.paid_orders_without_payment_rows', '存在已支付订单没有 payment 台账。', {
          count: paidWithoutPayment,
          paidOrders,
        });

    const paidWithoutUser = await this.scalar(
      `
      select count(*)
      from public.orders
      where tenant_id = $1 and status = 'paid' and user_id is null
      `,
      [this.tenant.id],
    );
    if (paidWithoutUser > 0) {
      this.warn('commerce.paid_orders_without_user', '存在已支付但缺用户的旧订单，导入器不会自动开权益，需要财务/运营人工复核。', {
        count: paidWithoutUser,
      });
    } else {
      this.pass('commerce.paid_orders_user_refs_resolved', '已支付订单均已解析到用户。');
    }

    const entitlementInvalid = await this.scalar(
      `
      select count(*)
      from public.entitlements e
      left join public.platform_users pu on pu.id = e.user_id
      where e.tenant_id = $1 and pu.id is null
      `,
      [this.tenant.id],
    );
    entitlementInvalid === 0
      ? this.pass('commerce.entitlements_user_refs_valid', '权益均引用有效用户。', { entitlements: this.counts.entitlements })
      : this.fail('commerce.entitlements_user_refs_invalid', '存在权益引用无效用户。', { count: entitlementInvalid });

    if (this.counts.entitlements > 0) {
      const activeEntitlement = await this.one(
        `
        select id, entitlement_type as "entitlementType", scope_type as "scopeType", status
        from public.entitlements
        where tenant_id = $1
          and status = 'active'
          and (expires_at is null or expires_at > now())
        order by created_at desc
        limit 1
        `,
        [this.tenant.id],
      );
      if (activeEntitlement) {
        this.samples.entitlementId = activeEntitlement.id;
        this.pass('commerce.active_entitlement_available', '存在可用于会员权限联调的 active 权益。', {
          entitlementType: activeEntitlement.entitlementType,
          scopeType: activeEntitlement.scopeType,
        });
      } else {
        this.warn('commerce.no_current_active_entitlement', '权益数据已迁移，但当前没有未过期 active 权益；学生会员态联调需新开通测试权益。');
      }
    } else {
      this.warn('commerce.entitlements_empty', '没有迁移权益数据，会员态需要通过新订单/激活码测试。');
    }

    if (this.counts.activation_codes > 0) {
      const usedCodeWithoutUser = await this.scalar(
        `
        select count(*)
        from public.activation_codes
        where tenant_id = $1 and is_used = true and used_by is null
        `,
        [this.tenant.id],
      );
      usedCodeWithoutUser === 0
        ? this.pass('commerce.activation_codes_user_refs_valid', '已使用激活码均保留使用人引用。', { count: this.counts.activation_codes })
        : this.warn('commerce.activation_codes_used_without_user', '部分旧激活码标记已使用但缺使用人，需要运营复核。', {
            count: usedCodeWithoutUser,
          });
    } else {
      this.warn('commerce.activation_codes_empty', '没有激活码迁移数据。');
    }
  }

  async checkAssetsAndVideos() {
    if (this.counts.content_assets > 0) {
      this.pass('assets.content_assets_available', '内容资源台账已迁移。', { count: this.counts.content_assets });
      const rawPathLeaks = await this.scalar(
        `
        select count(*)
        from public.content_assets
        where tenant_id = $1
          and (
            coalesce(cdn_url, '') ~* '([a-z]:\\\\|/users/|/home/|/root/|/var/)'
            or coalesce(preview_url, '') ~* '([a-z]:\\\\|/users/|/home/|/root/|/var/)'
            or coalesce(object_key, '') ~* '(^[a-z]:\\\\|^/users/|^/home/|^/root/|^/var/)'
          )
        `,
        [this.tenant.id],
      );
      rawPathLeaks === 0
        ? this.pass('assets.no_raw_local_path_leak', '资源公开字段未发现本地磁盘路径泄露。')
        : this.fail('assets.raw_local_path_leak', '资源公开字段疑似包含本地磁盘路径，必须改为对象存储 key 或受控 URL。', {
            count: rawPathLeaks,
          });

      const lockedExternalAssets = await this.scalar(
        `
        select count(*)
        from public.content_assets
        where tenant_id = $1
          and status = 'active'
          and visibility in ('members', 'svip', 'private')
          and storage_provider = 'external_url'
          and coalesce(metadata->>'providerManagedAccess', 'false') <> 'true'
          and coalesce(access_rules->>'cdnAccessMode', '') <> 'signed_by_provider'
        `,
        [this.tenant.id],
      );
      if (lockedExternalAssets > 0) {
        this.warn('assets.locked_external_urls_need_provider_policy', '部分会员/私有资源仍是外部 URL，下载预览会被后端安全策略限制；上线前应迁入 OSS/COS/Supabase Storage 或配置 provider-managed 签名。', {
          count: lockedExternalAssets,
        });
      } else {
        this.pass('assets.locked_assets_access_policy_ok', '会员/私有资源未发现不受控外部 URL。');
      }
    } else {
      this.warn('assets.content_assets_empty', '没有内容资源台账；题图/PDF/视频后续必须通过 content_assets 管理。');
    }

    if (this.counts.video_explanations === 0 && this.counts.question_videos === 0) {
      this.skip('videos.not_present', '当前真实旧库没有题目视频数据，视频会员链路保留为后续导入/配置测试。');
      return;
    }

    const brokenVideoLinks = await this.scalar(
      `
      select count(*)
      from public.question_videos qv
      left join public.questions q on q.id = qv.question_id and q.tenant_id = qv.tenant_id
      left join public.video_explanations ve on ve.id = qv.video_id and ve.tenant_id = qv.tenant_id
      where qv.tenant_id = $1
        and (q.id is null or ve.id is null)
      `,
      [this.tenant.id],
    );
    brokenVideoLinks === 0
      ? this.pass('videos.question_video_refs_valid', '题目视频绑定均可解析到题目和视频。', {
          videos: this.counts.video_explanations,
          bindings: this.counts.question_videos,
        })
      : this.fail('videos.question_video_refs_invalid', '存在题目视频绑定引用无效题目或视频。', { count: brokenVideoLinks });
  }

  async checkImportIssues() {
    if (!this.latestRun) return;

    const criticalRows = await this.many(
      `
      select collection_name as "collectionName", issue_code as "issueCode", count(*)::integer as count
      from public.pb_import_issues
      where tenant_id = $1 and run_id = $2 and severity = 'critical'
      group by collection_name, issue_code
      order by collection_name asc, issue_code asc
      `,
      [this.tenant.id, this.latestRun.id],
    );

    const unexpected = criticalRows.filter(row => !ALLOWED_CRITICAL_IMPORT_ISSUES.has(`${row.collectionName}:${row.issueCode}`));
    if (unexpected.length > 0) {
      this.fail('import.unexpected_critical_issues', '最新导入存在未列入隔离策略的 critical issue。', {
        groups: unexpected,
      });
    } else if (criticalRows.length > 0) {
      this.warn('import.allowed_critical_issues_need_manual_review', '最新导入仅剩已知人工复核类 critical issue，正式切换前仍必须处理。', {
        groups: criticalRows,
      });
    } else {
      this.pass('import.no_critical_issues', '最新导入没有 critical issue。');
    }

    const warningCount = await this.scalar(
      `
      select count(*)
      from public.pb_import_issues
      where tenant_id = $1 and run_id = $2 and severity in ('warning', 'error')
      `,
      [this.tenant.id, this.latestRun.id],
    );
    if (warningCount > 0) {
      this.warn('import.warning_or_error_issues_present', '最新导入仍有 warning/error issue，需要在上线复核表中留处理结论。', {
        count: warningCount,
      });
    } else {
      this.pass('import.no_warning_or_error_issues', '最新导入没有 warning/error issue。');
    }
  }

  async checkSensitiveDataLeaks() {
    const publicProfileSensitive = await this.scalar(
      `
      select count(*)
      from public.platform_users
      where raw_profile::text ~* '"(password|tokenKey|secret|sessionKey|openId|unionId|wechatSessionKey|qqOpenId|wechatOpenId|wechatUnionId)"'
      `,
    );
    publicProfileSensitive === 0
      ? this.pass('security.public_profile_sensitive_keys_absent', 'platform_users.raw_profile 未发现敏感身份字段。')
      : this.fail('security.public_profile_sensitive_keys_present', 'platform_users.raw_profile 仍包含敏感身份/凭据字段。', {
          count: publicProfileSensitive,
        });

    const publicSettingsSensitive = await this.scalar(
      `
      select count(*)
      from public.tenant_settings
      where tenant_id = $1
        and public_config::text ~* '(secret|privatekey|sessionkey|accesskey|appkey|apikey|api_v3_key|notifytoken|aeskey)'
      `,
      [this.tenant.id],
    );
    publicSettingsSensitive === 0
      ? this.pass('security.tenant_public_settings_no_secret_keys', '租户 public_config 未发现敏感 key。')
      : this.fail('security.tenant_public_settings_secret_keys', '租户 public_config 疑似包含密钥字段，应迁入 app_private.tenant_secrets。', {
          count: publicSettingsSensitive,
        });

    const insecureCrmSecretRef = await this.scalar(
      `
      select count(*)
      from public.crm_config
      where tenant_id = $1
        and secret_ref is not null
        and secret_ref !~ '^app_private\\.tenant_secrets:'
      `,
      [this.tenant.id],
    );
    insecureCrmSecretRef === 0
      ? this.pass('security.crm_secret_refs_private', 'CRM secret_ref 均指向私密密钥引用。')
      : this.fail('security.crm_secret_refs_unsafe', 'CRM secret_ref 存在不安全引用。', { count: insecureCrmSecretRef });
  }

  buildReport() {
    const summary = this.checks.reduce(
      (acc, check) => {
        acc[check.status] += 1;
        return acc;
      },
      { pass: 0, warn: 0, fail: 0, skip: 0 },
    );
    return {
      generatedAt: new Date().toISOString(),
      config: {
        tenantId: this.tenant.id,
        tenantSlug: this.tenant.slug,
        sampleLimit: SAMPLE_LIMIT,
        failOnWarnings: FAIL_ON_WARNINGS,
      },
      tenant: this.tenant,
      latestRun: this.latestRun,
      counts: this.counts,
      rawCounts: this.rawCounts,
      samples: this.samples,
      summary,
      checks: this.checks,
    };
  }
}

function markdownReport(report) {
  const lines = [];
  lines.push('# PocketBase 真实迁移业务抽样验收报告');
  lines.push('');
  lines.push(`生成时间：${new Date(report.generatedAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`);
  lines.push('');
  lines.push('## 目标');
  lines.push('');
  lines.push(`- 租户：${report.tenant.slug} / ${report.tenant.name}`);
  lines.push(`- 最新导入：${report.latestRun ? `${report.latestRun.status} / ${report.latestRun.sourceName}` : '未找到'}`);
  lines.push(`- 抽样条数：${report.config.sampleLimit}`);
  lines.push('');
  lines.push('## 结果');
  lines.push('');
  lines.push('| 状态 | 数量 |');
  lines.push('| --- | ---: |');
  lines.push(`| PASS | ${report.summary.pass} |`);
  lines.push(`| WARN | ${report.summary.warn} |`);
  lines.push(`| FAIL | ${report.summary.fail} |`);
  lines.push(`| SKIP | ${report.summary.skip} |`);
  lines.push('');
  lines.push('## 租户数据规模');
  lines.push('');
  lines.push('| 表 | 记录数 |');
  lines.push('| --- | ---: |');
  for (const [table, count] of Object.entries(report.counts).sort(([a], [b]) => a.localeCompare(b))) {
    lines.push(`| ${table} | ${count} |`);
  }
  lines.push('');
  lines.push('## 检查项');
  lines.push('');
  lines.push('| 状态 | 检查项 | 说明 |');
  lines.push('| --- | --- | --- |');
  for (const check of report.checks) {
    lines.push(`| ${check.status.toUpperCase()} | ${check.name} | ${String(check.message).replaceAll('|', '\\|')} |`);
  }
  lines.push('');
  lines.push('## 说明');
  lines.push('');
  lines.push('- 该脚本只读数据库，不修复数据，不生成测试数据。');
  lines.push('- WARN 代表旧数据或生产配置需要人工复核；FAIL 代表当前新业务模型无法安全消费该部分迁移数据。');
  lines.push('- 报告目录已被 `.gitignore` 忽略，不应提交含真实业务数据的报告。');
  lines.push('');
  return `${lines.join('\n')}\n`;
}

async function writeReport(report) {
  await fs.mkdir(OUTPUT_DIR, { recursive: true });
  const stamp = shanghaiTimestampForFile(new Date(report.generatedAt));
  const jsonPath = path.join(OUTPUT_DIR, `pb-business-sample-${stamp}.json`);
  const mdPath = path.join(OUTPUT_DIR, `pb-business-sample-${stamp}.md`);
  await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
  await fs.writeFile(mdPath, markdownReport(report), 'utf8');
  return { jsonPath, mdPath };
}

function printConsole(report) {
  for (const check of report.checks) {
    const prefix = check.status.toUpperCase();
    console.log(`[${prefix}] ${check.name}: ${check.message}`);
  }
  console.log(
    `Business sample complete: ${report.summary.fail} failures, ${report.summary.warn} warnings, ${report.summary.skip} skipped, ${report.summary.pass} passed.`,
  );
}

async function main() {
  const pool = new pg.Pool({ connectionString: DATABASE_URL, max: 1 });
  const client = await pool.connect();
  try {
    await client.query('set default_transaction_read_only = on');
    const sampler = new BusinessSampler(client);
    const report = await sampler.run();
    printConsole(report);
    if (WRITE_REPORT) {
      const files = await writeReport(report);
      console.log(`[sample] wrote ${files.jsonPath}`);
      console.log(`[sample] wrote ${files.mdPath}`);
    }
    if (report.summary.fail > 0 || (FAIL_ON_WARNINGS && report.summary.warn > 0)) {
      process.exitCode = 1;
    }
  } finally {
    client.release();
    await pool.end();
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});
