import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import {
  Prisma,
  RobotLifecycleStage,
  RobotLifecycleEventType,
} from '@prisma/client';
import {
  ActivateSnDto,
  CreateRobotUnitDto,
  QueryRobotUnitDto,
  UpdateRobotUnitDto,
} from '../dto/robot-unit.dto';
import { RobotSystemConfigService, FfsnRule } from './robot-system-config.service';
import { SnapshotProjectorService } from './snapshot-projector.service';
import { RobotError } from '../errors/robot-manager.errors';
import { SkipAssertAccess } from '@common/decorators/skip-assert-access.decorator';

/**
 * RobotUnit CRUD（v3）
 *
 * RobotUnit 只存"不变属性"：
 *   - 身份：ffsn / supplierSn / placeholderSnOrig
 *   - 物料：modelId / skuId / usageType
 *   - 采购追溯：purchaseOrderId/Line / originalSupplierId / manufactureDate
 *   - 退役元数据：retiredAt / disposalType / disposalNotes
 *   - 外部 anchor：sapMaterialNo
 *
 * 可变状态见 RobotUnitSnapshot；历史变更见 RobotLifecycleEvent。
 * 创建时自动建 Snapshot + 写入第一条 stage_changed 事件（toStage=SUPPLY_PO_CREATED）。
 */
@Injectable()
export class RobotUnitService {
  private readonly logger = new Logger(RobotUnitService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly configService: RobotSystemConfigService,
    private readonly projector: SnapshotProjectorService,
  ) {}

  // ---------------- FFSN 生成 ----------------

  private formatDatePart(format: FfsnRule['dateFormat'], now: Date): string {
    const y = now.getFullYear();
    const m = String(now.getMonth() + 1).padStart(2, '0');
    const d = String(now.getDate()).padStart(2, '0');
    switch (format) {
      case 'YYYY':
        return `${y}`;
      case 'YYYYMMDD':
        return `${y}${m}${d}`;
      case 'YYYYMM':
      default:
        return `${y}${m}`;
    }
  }

  private async generateFfsn(tx: Prisma.TransactionClient, organizationId: string): Promise<string> {
    const rule = await this.configService.getFfsnRule();
    const datePart = this.formatDatePart(rule.dateFormat, new Date());
    const prefix = `${rule.prefix}-${datePart}-`;

    const latest = await tx.robotUnit.findFirst({
      where: { organizationId, ffsn: { startsWith: prefix } },
      orderBy: { ffsn: 'desc' },
      select: { ffsn: true },
    });

    let seq = 1;
    if (latest) {
      const lastSeq = parseInt(latest.ffsn.substring(prefix.length), 10);
      if (!isNaN(lastSeq)) seq = lastSeq + 1;
    }

    return `${prefix}${String(seq).padStart(rule.seqLength, '0')}`;
  }

  private async validateForeignKeys(dto: {
    modelId?: string;
    skuId?: string;
    purchaseOrderId?: string;
    purchaseOrderLineId?: string;
  }) {
    const [model, sku, po, line] = await Promise.all([
      dto.modelId ? this.prisma.robotModel.findUnique({ where: { id: dto.modelId } }) : null,
      dto.skuId ? this.prisma.robotSku.findUnique({ where: { id: dto.skuId } }) : null,
      dto.purchaseOrderId ? this.prisma.purchaseOrder.findUnique({ where: { id: dto.purchaseOrderId } }) : null,
      dto.purchaseOrderLineId
        ? this.prisma.purchaseOrderLine.findUnique({ where: { id: dto.purchaseOrderLineId } })
        : null,
    ]);
    if (dto.modelId && !model) throw new BadRequestException(`Model ${dto.modelId} not found`);
    if (dto.skuId) {
      if (!sku) throw new BadRequestException(`Sku ${dto.skuId} not found`);
      if (dto.modelId && sku.modelId !== dto.modelId) {
        throw new BadRequestException(`Sku ${dto.skuId} does not belong to model ${dto.modelId}`);
      }
    }
    if (dto.purchaseOrderId && !po) {
      throw new BadRequestException(`PurchaseOrder ${dto.purchaseOrderId} not found`);
    }
    if (dto.purchaseOrderLineId) {
      if (!line) throw new BadRequestException(`PurchaseOrderLine ${dto.purchaseOrderLineId} not found`);
      if (dto.purchaseOrderId && line.purchaseOrderId !== dto.purchaseOrderId) {
        throw new BadRequestException(
          `Line ${dto.purchaseOrderLineId} does not belong to PO ${dto.purchaseOrderId}`,
        );
      }
    }
  }

  // ---------------- Create ----------------

  async create(dto: CreateRobotUnitDto, userId: string, organizationId: string) {
    await this.validateForeignKeys(dto);

    const unitId = await this.prisma.$transaction(async (tx) => {
      const ffsn = dto.ffsn ?? (await this.generateFfsn(tx, organizationId));

      const unit = await tx.robotUnit.create({
        data: {
          organizationId,
          ffsn,
          placeholderSnOrig: dto.placeholderSnOrig,
          supplierSn: dto.supplierSn,
          modelId: dto.modelId,
          skuId: dto.skuId,
          usageType: dto.usageType,
          purchaseOrderId: dto.purchaseOrderId,
          purchaseOrderLineId: dto.purchaseOrderLineId,
          originalSupplierId: dto.originalSupplierId,
          manufactureDate: dto.manufactureDate ? new Date(dto.manufactureDate) : null,
          sapMaterialNo: dto.sapMaterialNo,
          metadata: (dto.metadata ?? {}) as Prisma.InputJsonValue,
          createdById: userId,
        },
      });

      const firstEvent = await tx.robotLifecycleEvent.create({
        data: {
          robotUnitId: unit.id,
          eventType: RobotLifecycleEventType.stage_changed,
          fromStage: null,
          toStage: RobotLifecycleStage.SUPPLY_PO_CREATED,
          actorUserId: userId,
          notes: '新建机器人',
          payload: {} as Prisma.InputJsonValue,
          occurredAt: new Date(),
          organizationId,
          createdById: userId,
        },
      });

      await this.projector.project(tx, firstEvent, undefined); // 新建 snapshot

      this.logger.log(`Created robot unit ${ffsn} (id=${unit.id})`);
      return unit.id;
    });

    return this.findOne(unitId);
  }

  // ---------------- 占位 SN 激活（07 RECEIVED）----------------

  /** 占位 SN 识别：当前 ffsn 是占位格式（含 -LINE-NNN 后缀，且没 placeholderSnOrig 记录） */
  private isPlaceholderSn(unit: { ffsn: string; placeholderSnOrig: string | null }): boolean {
    return /-LINE-\d+$/.test(unit.ffsn) && !unit.placeholderSnOrig;
  }

  /**
   * 占位 SN 激活（v3 业务设计：07 RECEIVED 扫供应商物理标签）
   * - 校验：必须是占位 SN、当前 stage 必须 ≤ CUSTOMS_CLEARED、supplierSn 必填
   * - 行为：ffsn = newFfsn（生成或 dto 传入），placeholderSnOrig = 原占位 SN，supplierSn 写入
   * - 事件：写入 sn_activated event
   * - 可选：dto.advanceTo 同事务推进 stage（如 WAREHOUSE_RECEIVED）
   */
  async activateSn(
    unitId: string,
    dto: ActivateSnDto,
    userId: string,
    organizationId: string,
  ) {
    const unit = await this.prisma.robotUnit.findFirst({
      where: { id: unitId, deletedAt: null },
      include: { snapshot: true },
    });
    if (!unit) throw new NotFoundException(`Unit ${unitId} not found`);
    if (!unit.snapshot) throw new BadRequestException('Snapshot 缺失（数据完整性异常）');
    const snapshot = unit.snapshot;  // narrow to non-null for TS

    if (!this.isPlaceholderSn(unit)) {
      throw new ConflictException({
        code: 'ALREADY_ACTIVATED',
        message: `SN ${unit.ffsn} 不是占位（已激活或非占位格式）`,
      });
    }
    if (!dto.supplierSn) {
      throw new BadRequestException({ code: 'MISSING_SUPPLIER_SN', message: 'supplierSn 必填' });
    }

    const ALLOWED_STAGES = new Set<RobotLifecycleStage>([
      RobotLifecycleStage.SUPPLY_PO_CREATED,
      RobotLifecycleStage.SUPPLY_IN_PRODUCTION,
      RobotLifecycleStage.SUPPLY_READY_TO_SHIP,
      RobotLifecycleStage.LOGISTICS_IN_TRANSIT,
      RobotLifecycleStage.LOGISTICS_BONDED,
      RobotLifecycleStage.LOGISTICS_CUSTOMS_CLEARED,
    ]);
    if (!ALLOWED_STAGES.has(snapshot.currentStage)) {
      throw new ConflictException({
        code: 'STAGE_NOT_AVAILABLE',
        message: `不能在 ${snapshot.currentStage} 阶段激活（必须 ≤ CUSTOMS_CLEARED）`,
      });
    }

    return this.prisma.$transaction(async (tx) => {
      const newFfsn = dto.ffsn ?? (await this.generateFfsn(tx, organizationId));

      // 检查正式 ffsn 是否冲突（其他 unit 已用此 ffsn）
      const conflict = await tx.robotUnit.findFirst({
        where: { organizationId, ffsn: newFfsn, deletedAt: null, NOT: { id: unitId } },
        select: { id: true },
      });
      if (conflict) {
        throw new ConflictException({
          code: 'SN_CONFLICT',
          message: `正式 SN ${newFfsn} 已被其他 unit 占用`,
        });
      }

      await tx.robotUnit.update({
        where: { id: unitId },
        data: {
          ffsn: newFfsn,
          placeholderSnOrig: unit.ffsn,
          supplierSn: dto.supplierSn,
        },
      });

      const activateEvent = await tx.robotLifecycleEvent.create({
        data: {
          robotUnitId: unitId,
          eventType: RobotLifecycleEventType.sn_activated,
          fromStage: snapshot.currentStage,
          toStage: snapshot.currentStage,
          actorUserId: userId,
          notes:
            dto.reason ??
            `占位 SN ${unit.ffsn} 激活为正式 SN ${newFfsn}`,
          payload: {
            placeholderSn: unit.ffsn,
            newFfsn,
            supplierSn: dto.supplierSn,
          } as Prisma.InputJsonValue,
          occurredAt: new Date(),
          organizationId,
          createdById: userId,
        },
      });
      await this.projector.project(tx, activateEvent, snapshot.version);

      // 可选：同事务推进 stage
      if (dto.advanceTo && dto.advanceTo !== snapshot.currentStage) {
        const stageEvent = await tx.robotLifecycleEvent.create({
          data: {
            robotUnitId: unitId,
            eventType: RobotLifecycleEventType.stage_changed,
            fromStage: snapshot.currentStage,
            toStage: dto.advanceTo,
            actorUserId: userId,
            notes: `激活同步推进到 ${dto.advanceTo}`,
            payload: {} as Prisma.InputJsonValue,
            occurredAt: new Date(),
            organizationId,
            createdById: userId,
          },
        });
        await this.projector.project(tx, stageEvent, snapshot.version + 1);
      }

      this.logger.log(`activated SN unit=${unitId} ${unit.ffsn} → ${newFfsn} by=${userId}`);
      return tx.robotUnit.findUnique({ where: { id: unitId }, include: { snapshot: true } });
    });
  }

  // ---------------- Query (findAll) ----------------

  async findAll(query: QueryRobotUnitDto) {
    const {
      page = 1,
      limit = 20,
      sortBy = 'createdAt',
      sortOrder = 'desc',
      currentStage,
      modelId,
      skuId,
      purchaseOrderId,
      currentLocationId,
      currentCustomerId,
      isHeld,
      includeDeleted,
      search,
    } = query;

    const where: Prisma.RobotUnitWhereInput = {
      ...(includeDeleted ? {} : { deletedAt: null }),
    };
    if (modelId) where.modelId = modelId;
    if (skuId) where.skuId = skuId;
    if (purchaseOrderId) where.purchaseOrderId = purchaseOrderId;
    if (search) {
      where.OR = [
        { ffsn: { contains: search, mode: 'insensitive' } },
        { supplierSn: { contains: search, mode: 'insensitive' } },
      ];
    }

    const snapshotFilter: Prisma.RobotUnitSnapshotWhereInput = {};
    if (currentStage && currentStage.length) snapshotFilter.currentStage = { in: currentStage };
    if (currentLocationId) snapshotFilter.currentLocationId = currentLocationId;
    if (currentCustomerId) snapshotFilter.currentCustomerId = currentCustomerId;
    if (isHeld !== undefined) snapshotFilter.isHeld = isHeld;
    if (Object.keys(snapshotFilter).length > 0) {
      where.snapshot = snapshotFilter;
    }

    const skip = (page - 1) * limit;

    const [items, total] = await Promise.all([
      this.prisma.robotUnit.findMany({
        where,
        skip,
        take: limit,
        orderBy: { [sortBy]: sortOrder },
        include: {
          model: { select: { id: true, code: true, name: true } },
          sku: { select: { id: true, code: true, name: true, variant: true } },
          snapshot: true,
          purchaseOrder: { select: { id: true, poNo: true, orderedAt: true, supplierId: true } },
          purchaseOrderLine: { select: { id: true, lineNo: true, unitPrice: true, currencyCode: true } },
        },
      }),
      this.prisma.robotUnit.count({ where }),
    ]);

    const totalPages = Math.ceil(total / limit);

    return {
      items,
      total,
      page,
      limit,
      totalPages,
      hasNext: page < totalPages,
      hasPrev: page > 1,
    };
  }

  // ---------------- findOne by ffsn (QR deep link) ----------------

  async findByFfsn(ffsn: string) {
    const unit = await this.prisma.robotUnit.findFirst({
      where: { ffsn, deletedAt: null },
      select: { id: true, ffsn: true },
    });
    if (!unit) throw new NotFoundException(`Robot unit ffsn=${ffsn} not found`);
    return unit;
  }

  // ---------------- findOne ----------------

  async findOne(id: string) {
    const unit = await this.prisma.robotUnit.findFirst({
      where: { id, deletedAt: null },
      include: {
        model: true,
        sku: { include: { model: { select: { id: true, code: true, name: true } } } },
        snapshot: true,
        events: { orderBy: { occurredAt: 'desc' }, take: 50 },
        purchaseOrder: { select: { id: true, poNo: true, supplierId: true } },
        purchaseOrderLine: { select: { id: true, lineNo: true, unitPrice: true } },
      },
    });
    if (!unit) throw new NotFoundException(`Robot unit ${id} not found`);
    return unit;
  }

  // ---------------- Update ----------------

  @SkipAssertAccess('上方 findFirst({ id, deletedAt: null }) 已经按 id 抓到当前对象，紧接的 update 共用同一 id 不会跨 owner；DataScope 经 organizationId 索引隔离')
  async update(id: string, dto: UpdateRobotUnitDto, userId: string) {
    const existing = await this.prisma.robotUnit.findFirst({
      where: { id, deletedAt: null },
    });
    if (!existing) throw new NotFoundException(`Robot unit ${id} not found`);

    if (dto.version !== undefined && existing.version !== dto.version) {
      throw new ConflictException({
        code: RobotError.VERSION_CONFLICT,
        message: 'Data was modified by another user, please refresh and retry',
      });
    }

    const updates: Prisma.RobotUnitUncheckedUpdateInput = {
      version: { increment: 1 },
    };
    if (dto.supplierSn !== undefined) updates.supplierSn = dto.supplierSn;
    if (dto.usageType !== undefined) updates.usageType = dto.usageType;
    if (dto.manufactureDate !== undefined) {
      updates.manufactureDate = dto.manufactureDate ? new Date(dto.manufactureDate) : null;
    }
    if (dto.sapMaterialNo !== undefined) updates.sapMaterialNo = dto.sapMaterialNo;
    if (dto.disposalType !== undefined) {
      updates.disposalType = dto.disposalType;
      // 自动盖 retiredAt
      if (dto.disposalType && !existing.retiredAt) updates.retiredAt = new Date();
    }
    if (dto.disposalNotes !== undefined) updates.disposalNotes = dto.disposalNotes;
    if (dto.metadata !== undefined) {
      const existingMeta = (existing.metadata ?? {}) as Record<string, any>;
      updates.metadata = { ...existingMeta, ...dto.metadata } as Prisma.InputJsonValue;
    }

    await this.prisma.robotUnit.update({ where: { id }, data: updates });

    // 若改了 usageType，写一条 usage_type_changed event
    if (dto.usageType !== undefined && dto.usageType !== existing.usageType) {
      await this.prisma.robotLifecycleEvent.create({
        data: {
          robotUnitId: id,
          eventType: RobotLifecycleEventType.usage_type_changed,
          actorUserId: userId,
          payload: { from: existing.usageType, to: dto.usageType } as Prisma.InputJsonValue,
          occurredAt: new Date(),
          organizationId: existing.organizationId,
          createdById: userId,
        },
      });
    }
    // 若激活了 supplierSn 走 sn_activated event
    if (dto.supplierSn !== undefined && dto.supplierSn && !existing.supplierSn) {
      await this.prisma.robotLifecycleEvent.create({
        data: {
          robotUnitId: id,
          eventType: RobotLifecycleEventType.sn_activated,
          actorUserId: userId,
          payload: { supplierSn: dto.supplierSn } as Prisma.InputJsonValue,
          occurredAt: new Date(),
          organizationId: existing.organizationId,
          createdById: userId,
        },
      });
    }

    return this.findOne(id);
  }

  // ---------------- Soft delete ----------------

  @SkipAssertAccess('上方 findFirst 已抓到当前对象，softDelete 用同一 id 设 deletedAt，无 IDOR 风险')
  async softDelete(id: string, userId: string) {
    const existing = await this.prisma.robotUnit.findFirst({
      where: { id, deletedAt: null },
    });
    if (!existing) throw new NotFoundException(`Robot unit ${id} not found`);

    await this.prisma.robotUnit.update({
      where: { id },
      data: { deletedAt: new Date() },
    });
    return { message: 'Robot unit deleted successfully' };
  }
}
