/**
 * EAI Robotics 组织架构一次性 seed 脚本。
 *
 * 模式：
 *   --scan       仅扫描，输出匹配率 + 未匹配清单，不动 DB（默认）
 *   --dry-run    完整模拟插入，输出统计，不写库
 *   --apply      真实执行
 *   --reset-dev  在 apply 前先 DELETE 现有 EAI 组织下所有部门 + user_department
 *                （仅 dev 生效，生产环境 process.env.NODE_ENV='production' 时强制禁用）
 *   --force      已 seed（存在 EAI 组织且 metadata.seededBy='eai-orgchart-v1'）时仍允许执行
 *
 * 用法：
 *   cd <repo-root>
 *   npm run seed:eai-orgchart -- --scan
 *   npm run seed:eai-orgchart -- --dry-run
 *   npm run seed:eai-orgchart -- --apply --reset-dev          # dev only
 *   NODE_ENV=production npm run seed:eai-orgchart -- --apply  # prod
 */

import { PrismaClient } from '@prisma/client';
import * as fs from 'fs';
import * as path from 'path';
import { randomUUID, createHash } from 'crypto';

const prisma = new PrismaClient();

const ARGS = process.argv.slice(2);
const MODE: 'scan' | 'dry-run' | 'apply' =
  ARGS.includes('--apply') ? 'apply' : ARGS.includes('--dry-run') ? 'dry-run' : 'scan';
const RESET_DEV = ARGS.includes('--reset-dev');
const FORCE = ARGS.includes('--force');
const IS_PROD = process.env.NODE_ENV === 'production';

if (RESET_DEV && IS_PROD) {
  console.error('🛑 --reset-dev is disabled when NODE_ENV=production');
  process.exit(2);
}

const SEED_VERSION = 'eai-orgchart-v1';
const ORG_CODE = 'EAI';
const ORG_NAME = 'EAI Robotics';

// Deterministic UUID v5-like: derive from sha1 of seed-namespace + code, keeps same ID across runs
function deterministicId(scope: string, key: string): string {
  const h = createHash('sha1').update(`${SEED_VERSION}:${scope}:${key}`).digest('hex');
  return [
    h.slice(0, 8),
    h.slice(8, 12),
    '5' + h.slice(13, 16),
    ((parseInt(h.slice(16, 17), 16) & 0x3) | 0x8).toString(16) + h.slice(17, 20),
    h.slice(20, 32),
  ].join('-');
}

// ─────────────────────────────────────────────────────────────
// Types matching demo data shape
// ─────────────────────────────────────────────────────────────
type DemoCat = 'exec' | 'eng' | 'ops' | 'biz' | 'corp' | 'fin';
interface DemoDept {
  code: string;
  name: string;
  leader?: string;
  cat?: DemoCat;
  t2?: DemoDept[];
  t3?: DemoDept[];
  members?: string[];
  scope?: string;
}

interface SeedData {
  organization: { code: string; name: string };
  departments: DemoDept[];
}

interface AliasMap {
  byEmail: Record<string, string>;
}

// ─────────────────────────────────────────────────────────────
// Name resolution
// ─────────────────────────────────────────────────────────────
type ResolvedName = {
  user: { id: string; email: string; displayName: string };
  acting: boolean;
  via: 'alias' | 'display' | 'inverted';
};

function stripParens(s: string): string {
  return s.replace(/\s*\([^)]*\)\s*/g, '').trim();
}
function norm(s: string): string {
  return (s || '').toLowerCase().trim().replace(/\s+/g, ' ');
}
function looksLikePlaceholder(s: string): boolean {
  return /^(tbd|tbh|—|-|various|none|n\/a)$/i.test(s);
}
function looksLikeRole(s: string): boolean {
  if (/(Manager|Engineer|Designer|Analyst|Operations|Marketing|Conversion|Optimization|Specialist|Platform|Counsel|Counsellor)$/i.test(s)) return true;
  if (/^(Agent|Foundation|Skilled|Web Analytics|Source & |3rd Party|Ecommerce |commerce |Live Stream|Search Optimization|Matrix Account|Commercial Demo|E-commerce|VP\s|Finance\s+Manager)/i.test(s)) return true;
  if (/^(Manager,|Supervisor,)/i.test(s)) return true;
  return false;
}

function buildResolver(
  users: { id: string; email: string; displayName: string }[],
  aliases: AliasMap,
) {
  const byEmail = new Map<string, typeof users[number]>();
  const byDisplay = new Map<string, typeof users[number]>();
  users.forEach((u) => {
    if (u.email) byEmail.set(u.email.toLowerCase(), u);
    if (u.displayName) byDisplay.set(norm(u.displayName), u);
  });

  return function resolve(rawIn: string): ResolvedName | null {
    if (!rawIn) return null;
    const raw = rawIn.trim();
    const acting = /\(Acting\)/i.test(raw);

    // 1. alias map (exact raw match first, then stripped)
    const aliasEmail =
      aliases.byEmail[raw] ?? aliases.byEmail[stripParens(raw)];
    if (aliasEmail) {
      const u = byEmail.get(aliasEmail.toLowerCase());
      if (u) return { user: u, acting, via: 'alias' };
    }

    const stripped = stripParens(raw);
    if (!stripped) return null;
    if (looksLikePlaceholder(stripped)) return null;
    if (looksLikeRole(stripped)) return null;

    // 2. direct displayName match
    let u = byDisplay.get(norm(stripped));
    if (u) return { user: u, acting, via: 'display' };

    // 3. Last-First inversion: "Lopez Alcedo, Jose" → "Jose Lopez Alcedo"
    const m = stripped.match(/^([^,]+),\s*(.+)$/);
    if (m) {
      const last = m[1].trim();
      const first = m[2].replace(/\s+[A-Z]\.?$/, '').trim();
      const inverted = `${first} ${last}`;
      u = byDisplay.get(norm(inverted));
      if (u) return { user: u, acting, via: 'inverted' };
    }

    return null;
  };
}

// Parse a leader string ('A / B / C') → list of {raw, resolved?}
function parseLeaders(leaderStr: string | undefined, resolve: (s: string) => ResolvedName | null) {
  if (!leaderStr) return [];
  return leaderStr
    .split('/')
    .map((s) => s.trim())
    .filter(Boolean)
    .map((raw) => ({ raw, resolved: resolve(raw) }));
}

// Parse a member string: 'Role — Name' (em-dash) or just 'Name'
function parseMember(memberStr: string, resolve: (s: string) => ResolvedName | null) {
  const parts = memberStr.split(/\s*—\s*/);
  const nameRaw = parts.length > 1 ? parts[parts.length - 1].trim() : memberStr.trim();
  return { raw: memberStr, nameRaw, resolved: resolve(nameRaw) };
}

// ─────────────────────────────────────────────────────────────
// Department tree walker
// ─────────────────────────────────────────────────────────────
interface PlannedDept {
  id: string;
  code: string;
  name: string;
  parentId: string | null;
  headId: string | null;
  description: string | null;
  order: number;
  metadata: Record<string, unknown>;
  organizationId: string;
}
interface PlannedMembership {
  userId: string;
  departmentId: string;
  organizationId: string;
}

function walkAndPlan(
  data: SeedData,
  organizationId: string,
  resolve: (s: string) => ResolvedName | null,
) {
  const departments: PlannedDept[] = [];
  const memberships: PlannedMembership[] = [];
  const unmatchedLeaders = new Map<string, string[]>(); // raw → [dept paths]
  const unmatchedMembers = new Map<string, string[]>();

  function recordUnmatched(map: Map<string, string[]>, raw: string, path: string) {
    if (!map.has(raw)) map.set(raw, []);
    map.get(raw)!.push(path);
  }

  function processOne(d: DemoDept, parentId: string | null, order: number, parentPath: string, parentCode: string | null) {
    const path = parentPath ? `${parentPath} / ${d.name}` : d.name;
    // Always derive id from full path so dept entries with placeholder code='—'
    // (Capital Markets Analyst, Legal Counsel, HR Team, etc.) don't collide.
    const id = deterministicId('dept', `${ORG_CODE}::${path}`);
    // Same for DB code column (org_id, code) is UNIQUE — placeholder '—' would
    // collide across 6 demo depts. Derive a slug from parent code + name.
    const rawCode = d.code ?? '';
    const isPlaceholder = !rawCode || /^[—\-]+$/.test(rawCode);
    const slug = d.name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 24).toUpperCase();
    const effectiveCode = isPlaceholder
      ? (parentCode ? `${parentCode}_${slug}` : slug || `AUTO_${order}`)
      : rawCode;
    const leaders = parseLeaders(d.leader, resolve);
    const matchedLeaders = leaders.filter((l) => l.resolved);

    const metadata: Record<string, unknown> = {};
    if (d.cat) metadata.category = d.cat;

    let headId: string | null = null;
    let acting = false;

    // single-lead vs multi-lead vs none
    if (leaders.length === 1 && matchedLeaders.length === 1) {
      headId = matchedLeaders[0].resolved!.user.id;
      acting = matchedLeaders[0].resolved!.acting;
      if (acting) metadata.acting = true;
    } else if (leaders.length > 1) {
      // multi-lead → head_id=null, all in metadata.coLeaders
      metadata.coLeaders = leaders.map((l) => ({
        name: l.raw,
        userId: l.resolved?.user.id ?? null,
        email: l.resolved?.user.email ?? null,
        acting: l.resolved?.acting ?? false,
      }));
    } else if (leaders.length === 1 && !matchedLeaders.length) {
      // single but unmatched → leave null, metadata.unresolvedLead
      metadata.unresolvedLead = leaders[0].raw;
    }

    // record misses (across both single and multi)
    leaders.forEach((l) => {
      if (l.resolved) return;
      // skip pure placeholders/roles - they're not "misses"
      const stripped = stripParens(l.raw);
      if (looksLikePlaceholder(stripped) || looksLikeRole(stripped)) return;
      recordUnmatched(unmatchedLeaders, l.raw, path);
    });

    // members
    (d.members ?? []).forEach((m) => {
      const parsed = parseMember(m, resolve);
      if (parsed.resolved) {
        memberships.push({
          userId: parsed.resolved.user.id,
          departmentId: id,
          organizationId,
        });
      } else {
        const stripped = stripParens(parsed.nameRaw);
        if (looksLikePlaceholder(stripped) || looksLikeRole(stripped)) return;
        recordUnmatched(unmatchedMembers, parsed.nameRaw, path);
      }
    });

    if (d.scope) metadata.scope = d.scope;

    if (rawCode && isPlaceholder) metadata.originalCode = rawCode;

    departments.push({
      id,
      code: effectiveCode,
      name: d.name,
      parentId,
      headId,
      description: d.scope ?? null, // demo "scope" → DB description
      order,
      metadata,
      organizationId,
    });

    // recurse
    const children = [...(d.t2 ?? []), ...(d.t3 ?? [])];
    children.forEach((c, i) => processOne(c, id, i, path, effectiveCode));
  }

  data.departments.forEach((t1, i) => processOne(t1, null, i, '', null));

  // Deduplicate memberships (same user can be referenced under multiple paths)
  const seenMembership = new Set<string>();
  const uniqueMemberships = memberships.filter((m) => {
    const k = `${m.userId}|${m.departmentId}`;
    if (seenMembership.has(k)) return false;
    seenMembership.add(k);
    return true;
  });

  return { departments, memberships: uniqueMemberships, unmatchedLeaders, unmatchedMembers };
}

// ─────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────
async function main() {
  const dataPath = path.resolve(__dirname, 'eai-orgchart-data.json');
  const aliasPath = path.resolve(__dirname, 'eai-orgchart-aliases.json');
  const data: SeedData = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
  const aliases: AliasMap = JSON.parse(fs.readFileSync(aliasPath, 'utf8'));

  console.log(`\n🌱 EAI Orgchart Seed — mode=${MODE}, reset-dev=${RESET_DEV}, NODE_ENV=${process.env.NODE_ENV ?? '(unset)'}`);
  console.log(`   Demo data: ${data.departments.length} T1 departments`);

  // 1. Load users
  const users = await prisma.user.findMany({
    select: { id: true, email: true, displayName: true },
  });
  console.log(`   Users in DB: ${users.length}`);

  // 2. Resolve all names
  const resolve = buildResolver(users as any, aliases);
  const orgId = deterministicId('org', ORG_CODE);
  const plan = walkAndPlan(data, orgId, resolve);

  console.log(`\n📋 Plan summary:`);
  console.log(`   Organization: ${ORG_NAME} (${orgId})`);
  console.log(`   Departments: ${plan.departments.length}`);
  console.log(`   User-Dept memberships: ${plan.memberships.length}`);
  console.log(`   Unmatched leaders: ${plan.unmatchedLeaders.size}`);
  console.log(`   Unmatched members: ${plan.unmatchedMembers.size}`);

  if (plan.unmatchedLeaders.size > 0) {
    console.log(`\n⚠️  Unmatched LEADER names (head_id will be NULL; metadata.unresolvedLead set):`);
    [...plan.unmatchedLeaders.entries()]
      .sort((a, b) => a[0].localeCompare(b[0]))
      .forEach(([raw, paths]) => {
        console.log(`   - "${raw}" → appears in: ${paths.slice(0, 3).join(', ')}${paths.length > 3 ? ` (+${paths.length - 3} more)` : ''}`);
      });
  }
  if (plan.unmatchedMembers.size > 0) {
    console.log(`\n⚠️  Unmatched MEMBER names (skipped from user_departments):`);
    [...plan.unmatchedMembers.entries()]
      .sort((a, b) => a[0].localeCompare(b[0]))
      .forEach(([raw, paths]) => {
        console.log(`   - "${raw}" → appears in: ${paths.slice(0, 3).join(', ')}${paths.length > 3 ? ` (+${paths.length - 3} more)` : ''}`);
      });
  }

  if (MODE === 'scan') {
    console.log(`\n✅ scan complete (no DB changes). Run with --dry-run for full simulation, --apply to execute.`);
    return;
  }

  // 3. Check for already-seeded
  const existingOrg = await prisma.organization.findFirst({ where: { code: ORG_CODE } });
  if (existingOrg) {
    const meta = (existingOrg.metadata ?? {}) as Record<string, unknown>;
    const alreadySeeded = meta.seededBy === SEED_VERSION;
    if (alreadySeeded && !FORCE && !RESET_DEV) {
      console.error(`\n🛑 Organization ${ORG_CODE} was already seeded by ${SEED_VERSION}.`);
      console.error(`   Use --force to re-apply (idempotent) or --reset-dev to wipe first (dev only).`);
      process.exit(3);
    }
  }

  if (MODE === 'dry-run') {
    console.log(`\n🧪 dry-run complete (no DB changes). Run with --apply to execute.`);
    return;
  }

  // 4. APPLY
  await prisma.$transaction(async (tx) => {
    // 4a. Reset (dev only)
    if (RESET_DEV) {
      if (IS_PROD) throw new Error('--reset-dev is forbidden when NODE_ENV=production');
      const existing = await tx.organization.findFirst({ where: { code: ORG_CODE } });
      if (existing) {
        console.log(`\n🗑️  --reset-dev: deleting existing ${ORG_CODE} org (cascade)`);
        const delDept = await tx.userDepartment.deleteMany({ where: { organizationId: existing.id } });
        const delDepts = await tx.department.deleteMany({ where: { organizationId: existing.id } });
        const delOrg = await tx.organization.delete({ where: { id: existing.id } });
        console.log(`   deleted: user_dept=${delDept.count}, dept=${delDepts.count}, org=1`);
      }
    }

    // 4b. Upsert organization
    console.log(`\n✏️  Upserting organization ${ORG_CODE}`);
    const org = await tx.organization.upsert({
      where: { id: orgId },
      create: {
        id: orgId,
        code: ORG_CODE,
        name: ORG_NAME,
        status: 'ACTIVE',
        isActive: true,
        order: 0,
        metadata: { seededBy: SEED_VERSION, seededAt: new Date().toISOString() },
      },
      update: {
        name: ORG_NAME,
        metadata: { seededBy: SEED_VERSION, seededAt: new Date().toISOString() },
      },
    });
    console.log(`   org.id = ${org.id}`);

    // 4c. Insert departments — parent-first so FKs resolve
    // Sort: roots first, then children by parent depth
    const byId = new Map(plan.departments.map((d) => [d.id, d]));
    const depth = new Map<string, number>();
    function getDepth(id: string): number {
      if (depth.has(id)) return depth.get(id)!;
      const d = byId.get(id)!;
      const r = d.parentId ? getDepth(d.parentId) + 1 : 0;
      depth.set(id, r);
      return r;
    }
    plan.departments.forEach((d) => getDepth(d.id));
    const sorted = [...plan.departments].sort((a, b) => (depth.get(a.id)! - depth.get(b.id)!) || a.order - b.order);

    let created = 0;
    for (const d of sorted) {
      await tx.department.upsert({
        where: { id: d.id },
        create: {
          id: d.id,
          organizationId: d.organizationId,
          code: d.code,
          name: d.name,
          parentId: d.parentId,
          headId: d.headId,
          description: d.description,
          order: d.order,
          metadata: d.metadata as any,
        },
        update: {
          name: d.name,
          parentId: d.parentId,
          headId: d.headId,
          description: d.description,
          order: d.order,
          metadata: d.metadata as any,
        },
      });
      created++;
    }
    console.log(`   departments upserted: ${created}`);

    // 4d. Insert user_departments (idempotent via composite unique)
    let memCreated = 0;
    for (const m of plan.memberships) {
      try {
        await tx.userDepartment.upsert({
          where: {
            userId_departmentId: { userId: m.userId, departmentId: m.departmentId },
          },
          create: {
            userId: m.userId,
            departmentId: m.departmentId,
            organizationId: m.organizationId,
            isPrimary: false,
          },
          update: {},
        });
        memCreated++;
      } catch (err: any) {
        // Some prisma schemas have differently-named composite unique
        if (err?.code === 'P2002' || err?.code === 'P2025') {
          // already exists or no unique constraint, fallback to manual insert
          const exists = await tx.userDepartment.findFirst({
            where: { userId: m.userId, departmentId: m.departmentId },
          });
          if (!exists) {
            await tx.userDepartment.create({
              data: {
                userId: m.userId,
                departmentId: m.departmentId,
                organizationId: m.organizationId,
                isPrimary: false,
              },
            });
            memCreated++;
          }
        } else {
          throw err;
        }
      }
    }
    console.log(`   user_departments upserted: ${memCreated}`);
  });

  console.log(`\n✅ apply complete.`);
}

main()
  .catch((e) => {
    console.error('❌ Seed failed:', e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
