#!/usr/bin/env node
/**
 * Sync local plaza posts (+ publications/pages/assets) to production.
 *
 * Usage:
 *   node scripts/sync-plaza-to-prod.mjs
 *   node scripts/sync-plaza-to-prod.mjs --dry-run
 *   node scripts/sync-plaza-to-prod.mjs --include-offline   # re-online offline publications
 *
 * Env:
 *   DATABASE_URL              local source (default from .env)
 *   PLAZA_PROD_DATABASE_URL   production MySQL (required)
 *   PLAZA_PROD_SSH            e.g. root@ssh105
 *   MINDSPACE_STORAGE_ROOT    local storage (default data/mindspace)
 *   PLAZA_PROD_STORAGE_ROOT   remote storage (default .../h5/data/mindspace)
 */
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import mysql from 'mysql2/promise';

const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const dryRun = process.argv.includes('--dry-run');
const includeOffline = process.argv.includes('--include-offline');

function loadEnvFile(filePath) {
  if (!fs.existsSync(filePath)) return;
  for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    const eq = trimmed.indexOf('=');
    if (eq < 0) continue;
    const key = trimmed.slice(0, eq).trim();
    const value = trimmed.slice(eq + 1).trim();
    if (!process.env[key]) process.env[key] = value;
  }
}

loadEnvFile(path.join(root, '../../.env.local'));
loadEnvFile(path.join(root, '.env'));

const sourceUrl = process.env.DATABASE_URL;
const targetUrl = process.env.PLAZA_PROD_DATABASE_URL;
const prodSsh = process.env.PLAZA_PROD_SSH ?? 'root@ssh105';
const localStorageRoot = path.resolve(
  process.env.MINDSPACE_STORAGE_ROOT ?? path.join(root, 'data', 'mindspace'),
);
const remoteStorageRoot =
  process.env.PLAZA_PROD_STORAGE_ROOT ?? '/root/tkmind_go/ui/h5/data/mindspace';

if (!sourceUrl) {
  console.error('缺少 DATABASE_URL');
  process.exit(1);
}
if (!targetUrl) {
  console.error('缺少 PLAZA_PROD_DATABASE_URL');
  process.exit(1);
}

function quoteId(value) {
  return mysql.escape(value);
}

async function resolveProdSpaceId(target, userId) {
  const [rows] = await target.query(`SELECT id FROM h5_user_spaces WHERE user_id = ? LIMIT 1`, [userId]);
  if (rows[0]?.id) return rows[0].id;
  throw new Error(`生产环境缺少用户 ${userId} 的 MindSpace，请先在 prod 初始化空间`);
}

async function buildProdCategoryMap(source, target, localCategoryIds) {
  if (localCategoryIds.length === 0) return new Map();
  const [localCats] = await source.query(`SELECT * FROM h5_space_categories WHERE id IN (?)`, [
    localCategoryIds,
  ]);
  const map = new Map();
  for (const cat of localCats) {
    const prodSpaceId = await resolveProdSpaceId(target, cat.user_id);
    const [prodCats] = await target.query(
      `SELECT id FROM h5_space_categories WHERE space_id = ? AND category_code = ? LIMIT 1`,
      [prodSpaceId, cat.category_code],
    );
    let prodCategoryId = prodCats[0]?.id;
    if (!prodCategoryId) {
      prodCategoryId = cat.id;
      await upsertRows(target, 'h5_space_categories', [{ ...cat, space_id: prodSpaceId }]);
    }
    map.set(cat.id, { spaceId: prodSpaceId, categoryId: prodCategoryId });
  }
  return map;
}

function remapSpaceFields(row, categoryMap) {
  const mapped = categoryMap.get(row.category_id);
  if (!mapped) return row;
  return { ...row, space_id: mapped.spaceId, category_id: mapped.categoryId };
}

async function fetchRows(conn, table, ids) {
  if (ids.length === 0) return [];
  const [rows] = await conn.query(`SELECT * FROM ${table} WHERE id IN (?)`, [ids]);
  return rows;
}

async function buildPlazaCategoryMap(source, target, plazaCategoryIds) {
  if (plazaCategoryIds.length === 0) return new Map();
  const [localCats] = await source.query(`SELECT id, slug FROM plaza_categories WHERE id IN (?)`, [
    plazaCategoryIds,
  ]);
  const map = new Map();
  for (const cat of localCats) {
    const [prodCats] = await target.query(`SELECT id FROM plaza_categories WHERE slug = ? LIMIT 1`, [
      cat.slug,
    ]);
    if (!prodCats[0]) throw new Error(`生产环境缺少 plaza 分类：${cat.slug}`);
    map.set(cat.id, prodCats[0].id);
  }
  return map;
}

async function upsertRows(conn, table, rows) {
  if (rows.length === 0) return 0;
  let count = 0;
  for (const row of rows) {
    const cols = Object.keys(row);
    const values = cols.map((col) => {
      const value = row[col];
      if (value instanceof Date) return value;
      if (value !== null && typeof value === 'object') return JSON.stringify(value);
      return value;
    });
    const placeholders = cols.map(() => '?').join(', ');
    const updates = cols.filter((c) => c !== 'id').map((c) => `${c}=VALUES(${c})`).join(', ');
    await conn.query(
      `INSERT INTO ${table} (${cols.join(', ')}) VALUES (${placeholders})
       ON DUPLICATE KEY UPDATE ${updates}`,
      values,
    );
    count += 1;
  }
  return count;
}

function storagePaths(keys) {
  const relPaths = new Set();
  for (const key of keys) {
    const normalized = String(key ?? '').replace(/^\/+/, '');
    if (!normalized.startsWith('users/')) continue;
    relPaths.add(normalized);
    relPaths.add(path.dirname(normalized));
  }
  return [...relPaths];
}

async function main() {
  const source = await mysql.createConnection(sourceUrl);
  const target = await mysql.createConnection(targetUrl);

  const [posts] = await source.query(
    `SELECT pp.*, pr.status AS publication_status
     FROM plaza_posts pp
     JOIN h5_publish_records pr ON pr.id = pp.publication_id
     WHERE pp.status = 'published'
     ORDER BY pp.published_at DESC`,
  );

  if (posts.length === 0) {
    console.log('本地没有已发布的广场帖子。');
    await source.end();
    await target.end();
    return;
  }

  const publicationIds = posts.map((p) => p.publication_id);
  const [publications] = await source.query(`SELECT * FROM h5_publish_records WHERE id IN (?)`, [
    publicationIds,
  ]);
  const pageIds = [...new Set(publications.map((p) => p.page_id))];
  const pageVersionIds = [...new Set(publications.map((p) => p.page_version_id))];
  const scanIds = [...new Set(publications.map((p) => p.security_scan_id))];

  const [pageVersions] = await source.query(`SELECT * FROM h5_page_versions WHERE id IN (?)`, [
    pageVersionIds,
  ]);
  const [pages] = await source.query(`SELECT * FROM h5_page_records WHERE id IN (?)`, [pageIds]);

  const assetIds = new Set();
  for (const version of pageVersions) {
    if (version.content_asset_id) assetIds.add(version.content_asset_id);
    if (version.bundle_asset_id) assetIds.add(version.bundle_asset_id);
  }
  const [assets] = await source.query(`SELECT * FROM h5_assets WHERE id IN (?)`, [[...assetIds]]);
  const [assetVersions] = await source.query(
    `SELECT * FROM h5_asset_versions WHERE asset_id IN (?)`,
    [[...assetIds]],
  );
  const [scans] = await source.query(`SELECT * FROM h5_security_scans WHERE id IN (?)`, [scanIds]);

  const spaceIds = [...new Set(pages.map((p) => p.space_id))];
  const categoryIds = [
    ...new Set([
      ...pages.map((p) => p.category_id),
      ...assets.map((a) => a.category_id),
    ]),
  ];
  const categoryMap = dryRun ? new Map() : await buildProdCategoryMap(source, target, categoryIds);
  const plazaCategoryMap = dryRun
    ? new Map()
    : await buildPlazaCategoryMap(
        source,
        target,
        [...new Set(posts.map((p) => p.category_id))],
      );

  const pagesToSync = pages.map((row) => {
    const next = remapSpaceFields(row, categoryMap);
    const assetIdSet = new Set(assets.map((a) => a.id));
    if (next.source_asset_id && !assetIdSet.has(next.source_asset_id)) {
      next.source_asset_id = null;
    }
    if (next.cover_image_asset_id && !assetIdSet.has(next.cover_image_asset_id)) {
      next.cover_image_asset_id = null;
    }
    return next;
  });
  const assetsToSync = assets.map((row) => remapSpaceFields(row, categoryMap));

  const offlineCount = publications.filter((p) => p.status !== 'online').length;
  const publicationsToSync = publications.map((row) => {
    if (row.status === 'online' || !includeOffline) return row;
    return { ...row, status: 'online', offline_at: null, updated_at: Date.now() };
  });

  const storageKeys = assetVersions.map((row) => row.storage_key).filter(Boolean);
  const filePaths = storagePaths(storageKeys);

  console.log(`准备同步 ${posts.length} 条广场帖（${publications.length} 个发布，${offlineCount} 个当前 offline）`);
  console.log(`  页面 ${pages.length} · 资产 ${assets.length} · 文件路径 ${filePaths.length}`);

  if (dryRun) {
    console.log('\n[dry-run] 将同步的帖子：');
    for (const post of posts) {
      const pub = publications.find((p) => p.id === post.publication_id);
      console.log(`  - ${post.title} (${pub?.status ?? '?'})`);
    }
    await source.end();
    await target.end();
    return;
  }

  await target.beginTransaction();
  try {
    const counts = {};
    counts.scans = await upsertRows(target, 'h5_security_scans', scans);
    counts.assets = await upsertRows(target, 'h5_assets', assetsToSync);
    counts.assetVersions = await upsertRows(target, 'h5_asset_versions', assetVersions);
    counts.pages = await upsertRows(target, 'h5_page_records', pagesToSync);
    counts.pageVersions = await upsertRows(target, 'h5_page_versions', pageVersions);
    counts.publications = await upsertRows(target, 'h5_publish_records', publicationsToSync);
    counts.plazaPosts = await upsertRows(
      target,
      'plaza_posts',
      posts.map(({ publication_status, ...post }) => ({
        ...post,
        category_id: plazaCategoryMap.get(post.category_id) ?? post.category_id,
      })),
    );

    const userIds = [...new Set(posts.map((p) => p.user_id))];
    for (const userId of userIds) {
      const [countRows] = await target.query(
        `SELECT COUNT(*) AS c FROM plaza_posts WHERE user_id = ? AND status = 'published'`,
        [userId],
      );
      await target.query(`UPDATE h5_users SET plaza_post_count = ?, updated_at = ? WHERE id = ?`, [
        countRows[0].c,
        Date.now(),
        userId,
      ]);
    }

    await target.commit();
    console.log('数据库同步完成：', counts);
  } catch (error) {
    await target.rollback();
    throw error;
  }

  await source.end();
  await target.end();

  if (filePaths.length === 0) {
    console.log('无需同步用户文件。');
    return;
  }

  const includeArgs = filePaths.map((rel) => `--include=${rel}`);
  includeArgs.push('--include=*/');
  const rsyncCmd = [
    'rsync -az',
    ...includeArgs,
    '--exclude=*',
    `${localStorageRoot}/`,
    `${prodSsh}:${remoteStorageRoot}/`,
  ].join(' ');

  console.log('==> 同步发布页面文件到 105...');
  execSync(rsyncCmd, { stdio: 'inherit' });

  console.log('\n✅ 广场内容已同步到生产环境');
  console.log('   首页: https://plaza.tkmind.cn/plaza');
  if (offlineCount > 0 && !includeOffline) {
    console.log(`\n⚠  有 ${offlineCount} 条帖子关联的发布为 offline，未纳入同步。`);
    console.log('   若也要迁移，请重跑：node scripts/sync-plaza-to-prod.mjs --include-offline');
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.message : error);
  process.exit(1);
});
