import { Inject, Injectable, Logger } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { GenomeTraitDetailModel } from "src/genome/entities/genome-trait-detail.entity";
import { GenomeRequestModel } from "src/genome/entities/genome-request.entity";
import { CowModel } from "src/cow/entities/cow.entity";
import { Repository, IsNull, In } from "typeorm";
import { MptModel } from "src/mpt/entities/mpt.entity";
import { MptDto } from "src/mpt/dto/mpt.dto";
import { parseNumber, parseDate } from "src/common/utils";
import { ExcelUtil } from "src/common/excel/excel.util";
import { createReadStream } from "fs";
import * as readline from 'readline';
import { GeneDetailModel } from "src/gene/entities/gene-detail.entity";





@Injectable()
export class AdminService {
  private readonly logger = new Logger(AdminService.name);

  constructor(
    // 유전체 분석 결과 Repository
    @InjectRepository(GenomeTraitDetailModel)
    private readonly genomeTraitDetailRepository: Repository<GenomeTraitDetailModel>,

    // 유전체 분석 의뢰 Repository
    @InjectRepository(GenomeRequestModel)
    private readonly genomeRequestRepository: Repository<GenomeRequestModel>,
    
    // 소 개체 Repository
    @InjectRepository(CowModel)
    private readonly cowRepository: Repository<CowModel>,

    // 혈액화학검사 결과 Repository
    @InjectRepository(MptModel)
    private readonly mptRepository: Repository<MptModel>,

    // 유전자 상세 정보 Repository
    @InjectRepository(GeneDetailModel)
    private readonly geneDetailRepository: Repository<GeneDetailModel>,

    @Inject(ExcelUtil)
    private readonly excelUtil: ExcelUtil,
  ) {}


  async batchInsertGenomeResult(file: Express.Multer.File) {
    this.logger.log(`[배치업로드] 유전체 분석 결과 파일 처리 시작: ${file.originalname}`);

    try {
      // 엑셀파일 로드
      const rawData = this.excelUtil.parseExcelData(file);

      const headerRow = rawData[0];
      const dataRows = rawData.slice(1);

      // 헤더에서 형질명 추출 및 인덱스 매핑
      // 컬럼 구조: A(samplename), B, C, D~DD(형질 데이터)
      // 형질 패턴: "XXXXX", "XXXXX_표준화육종가", "XXXXX_백분율"
      
      const traitMap = new Map<string, { 
        name: string; 
        valIndex: number | null; 
        ebvIndex: number | null; 
        percentileIndex: number | null; 
      }>();

      // 헤더 분석 (인덱스 3부터 시작, D열부터)
      for (let colIdx = 3; colIdx < headerRow.length && colIdx <= 107; colIdx++) {
        const headerValue = headerRow[colIdx];
        if (!headerValue || typeof headerValue !== 'string') {
          continue;
        }

        const trimmedHeader = headerValue.trim();
        
        // 형질명 추출 (접미사 제거)
        if (trimmedHeader.endsWith('_표준화육종가')) {
          const traitName = trimmedHeader.replace('_표준화육종가', '');
          if (!traitMap.has(traitName)) {
            traitMap.set(traitName, { name: traitName, valIndex: null, ebvIndex: null, percentileIndex: null });
          }
          traitMap.get(traitName)!.ebvIndex = colIdx;
        } else if (trimmedHeader.endsWith('_백분율')) {
          const traitName = trimmedHeader.replace('_백분율', '');
          if (!traitMap.has(traitName)) {
            traitMap.set(traitName, { name: traitName, valIndex: null, ebvIndex: null, percentileIndex: null });
          }
          traitMap.get(traitName)!.percentileIndex = colIdx;
        } else {
          // 형질명 단독 (trait_val)
          // 이미 존재하는 형질인지 확인 (접미사가 있는 경우 이미 추가됨)
          if (!traitMap.has(trimmedHeader)) {
            traitMap.set(trimmedHeader, { name: trimmedHeader, valIndex: null, ebvIndex: null, percentileIndex: null });
          }
          traitMap.get(trimmedHeader)!.valIndex = colIdx;
        }
      }

      this.logger.log(`[배치업로드] 형질 개수: ${traitMap.size}개`);

      // 데이터 행 처리
      const traitDataArray: Array<{
        cowId: string;
        traitName: string;
        traitVal: number | null;
        traitEbv: number | null;
        traitPercentile: number | null;
      }> = [];

      for (let rowIdx = 0; rowIdx < dataRows.length; rowIdx++) {
        const row = dataRows[rowIdx];
        
        // A열(인덱스 0): cowId (samplename)
        const cowId = row[0];
        if (!cowId || typeof cowId !== 'string' || !cowId.trim()) {
          this.logger.warn(`[배치업로드] ${rowIdx + 2}행: cowId가 없어 건너뜀`);
          continue;
        }

        const trimmedCowId = cowId.trim();

        // 각 형질별로 데이터 추출
        for (const [traitName, indices] of traitMap.entries()) {
          const traitVal = indices.valIndex !== null && row[indices.valIndex] !== null && row[indices.valIndex] !== undefined
            ? parseNumber(row[indices.valIndex])
            : null;
          
          const traitEbv = indices.ebvIndex !== null && row[indices.ebvIndex] !== null && row[indices.ebvIndex] !== undefined
            ? parseNumber(row[indices.ebvIndex])
            : null;
          
          const traitPercentile = indices.percentileIndex !== null && row[indices.percentileIndex] !== null && row[indices.percentileIndex] !== undefined
            ? parseNumber(row[indices.percentileIndex])
            : null;

          // 값이 하나라도 있으면 추가
          if (traitVal !== null || traitEbv !== null || traitPercentile !== null) {
            traitDataArray.push({
              cowId: trimmedCowId,
              traitName: traitName,
              traitVal: traitVal,
              traitEbv: traitEbv,
              traitPercentile: traitPercentile,
            });
          }
        }
      }

      this.logger.log(`[배치업로드] 파싱 완료: ${traitDataArray.length}개 형질 데이터`);

      // ============================================
      // 3단계: Json Array 각 객체 fk_request_no 추가
      // - fk_request_no 추가 방법 : 소 식별번호(cowId)로 조회 후 fk_request_no 추가
      // ============================================
      this.logger.log('[배치업로드] 3단계: fk_request_no 조회 중...');

      // 고유한 cowId 목록 추출
      const uniqueCowIds = [...new Set(traitDataArray.map(item => item.cowId))];
      this.logger.log(`[배치업로드] 고유 cowId 개수: ${uniqueCowIds.length}개`);

      // cowId별로 Cow와 GenomeRequest 조회
      const cowIdToRequestNoMap = new Map<string, number | null>();

      for (const cowId of uniqueCowIds) {
        try {
          // Step 1: cowId로 개체 조회
          const cow = await this.cowRepository.findOne({
            where: { cowId: cowId, delDt: IsNull() },
          });

          if (!cow) {
            this.logger.warn(`[배치업로드] cowId "${cowId}"에 해당하는 개체를 찾을 수 없습니다.`);
            cowIdToRequestNoMap.set(cowId, null);
            continue;
          }

          // Step 2: 해당 개체의 최신 분석 의뢰 조회
          const request = await this.genomeRequestRepository.findOne({
            where: { fkCowNo: cow.pkCowNo, delDt: IsNull() },
            order: { requestDt: 'DESC', regDt: 'DESC' },
          });

          if (!request) {
            this.logger.warn(`[배치업로드] cowId "${cowId}"에 해당하는 유전체 분석 의뢰를 찾을 수 없습니다.`);
            cowIdToRequestNoMap.set(cowId, null);
            continue;
          }

          cowIdToRequestNoMap.set(cowId, request.pkRequestNo);
        } catch (error) {
          this.logger.error(`[배치업로드] cowId "${cowId}" 조회 중 오류: ${error.message}`);
          cowIdToRequestNoMap.set(cowId, null);
        }
      }

      // fk_request_no 추가
      const traitDataWithRequestNo = traitDataArray.map(item => {
        const fkRequestNo = cowIdToRequestNoMap.get(item.cowId);
        return {
          ...item,
          fkRequestNo: fkRequestNo,
        };
      });

      // fk_request_no가 없는 데이터 필터링
      const validTraitData = traitDataWithRequestNo.filter(item => item.fkRequestNo !== null);
      const invalidCount = traitDataWithRequestNo.length - validTraitData.length;

      if (invalidCount > 0) {
        this.logger.warn(`[배치업로드] fk_request_no가 없는 데이터 ${invalidCount}건 제외`);
      }

      this.logger.log(`[배치업로드] 유효한 형질 데이터: ${validTraitData.length}건`);

      // ============================================
      // 4단계: 데이터 DB Insert : 비동기 batchInsert(upsert)
      // ============================================
      this.logger.log('[배치업로드] 4단계: DB 배치 삽입(upsert) 중...');

      let successCount = 0;
      let errorCount = 0;

      // 배치 크기 설정 (한 번에 처리할 데이터 수)
      const BATCH_SIZE = 100;
      
      for (let i = 0; i < validTraitData.length; i += BATCH_SIZE) {
        const batch = validTraitData.slice(i, i + BATCH_SIZE);
        
        try {
          // 각 배치를 upsert 처리
          const insertPromises = batch.map(async (item) => {
            try {
              // 기존 데이터 조회 (cowId와 traitName 기준)
              const existing = await this.genomeTraitDetailRepository.findOne({
                where: {
                  cowId: item.cowId,
                  traitName: item.traitName,
                  delDt: IsNull(),
                },
              });

              if (existing) {
                // 업데이트
                existing.fkRequestNo = item.fkRequestNo!;
                existing.traitVal = item.traitVal;
                existing.traitEbv = item.traitEbv;
                existing.traitPercentile = item.traitPercentile;
                await this.genomeTraitDetailRepository.save(existing);
              } else {
                // 삽입
                const newTraitDetail = this.genomeTraitDetailRepository.create({
                  fkRequestNo: item.fkRequestNo!,
                  cowId: item.cowId,
                  traitName: item.traitName,
                  traitVal: item.traitVal,
                  traitEbv: item.traitEbv,
                  traitPercentile: item.traitPercentile,
                });
                await this.genomeTraitDetailRepository.save(newTraitDetail);
              }
              return true;
            } catch (error) {
              this.logger.error(`[배치업로드] 데이터 저장 실패 (cowId: ${item.cowId}, traitName: ${item.traitName}): ${error.message}`);
              return false;
            }
          });

          const results = await Promise.all(insertPromises);
          successCount += results.filter(r => r === true).length;
          errorCount += results.filter(r => r === false).length;

          this.logger.log(`[배치업로드] 진행률: ${Math.min(i + BATCH_SIZE, validTraitData.length)}/${validTraitData.length}`);
        } catch (error) {
          this.logger.error(`[배치업로드] 배치 처리 중 오류: ${error.message}`);
          errorCount += batch.length;
        }
      }

      // ============================================
      // 5단계: 결과 로깅
      // ============================================
      this.logger.log(`[배치업로드] 처리 완료`);
      this.logger.log(`[배치업로드] 성공: ${successCount}건, 실패: ${errorCount}건, 제외: ${invalidCount}건`);

      return {
        success: true,
        total: traitDataArray.length,
        valid: validTraitData.length,
        successCount: successCount,
        errorCount: errorCount,
        excludedCount: invalidCount,
      };
    } catch (error) {
      this.logger.error(`[배치업로드] 처리 중 오류 발생: ${error.message}`);
      this.logger.error(error.stack);
      throw error;
    }
  }

  
  async batchInsertSnpTyping(file: Express.Multer.File) {
    this.logger.log(`[배치업로드] SNP 타이핑 결과 파일 처리 시작: ${file.originalname}`);

    try {
      // ============================================
      // 1단계: 텍스트 파일 스트림 읽기
      // ============================================
      this.logger.log('[배치업로드] 1단계: 텍스트 파일 스트림 읽기 중...');

      const stream = createReadStream(file.path, {
        encoding: 'utf-8',
        highWaterMark: 64 * 1024, // 64KB 버퍼
      });

      const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });

      let section: 'NONE' | 'HEADER' | 'DATA_WAIT_HEADER' | 'DATA' = 'NONE';
      let dataHeader: string[] | null = null;

      // 컬럼 인덱스 매핑
      const COLUMN_MAPPING = {
        SNP_NAME: 'SNP Name',
        SAMPLE_ID: 'Sample ID',
        ALLELE1: 'Allele1 - Top',
        ALLELE2: 'Allele2 - Top',
        CHR: 'Chr',
        POSITION: 'Position',
        SNP: 'SNP',
      };

      // 컬럼 인덱스 캐시 (헤더 읽은 후 한 번만 계산)
      let columnIndices: {
        snpNameIdx: number;
        sampleIdIdx: number;
        allele1Idx: number;
        allele2Idx: number;
        chrIdx: number;
        positionIdx: number;
        snpIdx: number;
      } | null = null;

      // ============================================
      // 2단계: 스트림 방식으로 파일 파싱 및 배치 단위 DB 저장
      // ============================================
      this.logger.log('[배치업로드] 2단계: 스트림 방식 파일 파싱 및 배치 저장 중...');

      // 배치 버퍼 (메모리에 일정 크기만 유지)
      const BATCH_SIZE = 5000; // 배치 크기 (메모리 사용량 제어)
      const batchBuffer: Array<{
        cowId: string;
        snpName: string;
        chromosome: string | null;
        position: string | null;
        snpType: string | null;
        allele1: string | null;
        allele2: string | null;
      }> = [];

      let totalRows = 0;
      let successCount = 0;
      let errorCount = 0;
      let skippedCount = 0;

      // 배치 저장 함수
      const flushBatch = async () => {
        if (batchBuffer.length === 0) return;

        try {
          // 배치 단위로 upsert 처리
          const insertPromises = batchBuffer.map(async (item) => {
            try {
              // 기존 데이터 조회 (cowId와 snpName 기준)
              const existing = await this.geneDetailRepository.findOne({
                where: {
                  cowId: item.cowId,
                  snpName: item.snpName,
                  delDt: IsNull(),
                },
              });

              if (existing) {
                // 업데이트
                existing.fkRequestNo = null; // cowId 조회 제거로 null 처리
                existing.chromosome = item.chromosome;
                existing.position = item.position;
                existing.snpType = item.snpType;
                existing.allele1 = item.allele1;
                existing.allele2 = item.allele2;
                await this.geneDetailRepository.save(existing);
              } else {
                // 삽입
                const newGeneDetail = this.geneDetailRepository.create({
                  cowId: item.cowId,
                  snpName: item.snpName,
                  fkRequestNo: null, // cowId 조회 제거로 null 처리
                  chromosome: item.chromosome,
                  position: item.position,
                  snpType: item.snpType,
                  allele1: item.allele1,
                  allele2: item.allele2,
                });
                await this.geneDetailRepository.save(newGeneDetail);
              }
              return true;
            } catch (error) {
              this.logger.error(`[배치업로드] 데이터 저장 실패 (cowId: ${item.cowId}, snpName: ${item.snpName}): ${error.message}`);
              return false;
            }
          });

          const results = await Promise.all(insertPromises);
          const batchSuccess = results.filter(r => r === true).length;
          const batchError = results.filter(r => r === false).length;

          successCount += batchSuccess;
          errorCount += batchError;

          this.logger.log(`[배치업로드] 배치 저장 완료: ${batchSuccess}건 성공, ${batchError}건 실패 (총 처리: ${totalRows}건)`);
        } catch (error) {
          this.logger.error(`[배치업로드] 배치 저장 중 오류: ${error.message}`);
          errorCount += batchBuffer.length;
        } finally {
          // 배치 버퍼 초기화 (메모리 해제)
          batchBuffer.length = 0;
        }
      };

      // 파일 라인별 처리
      for await (const rawLine of rl) {
        const line = rawLine.trim();
        if (!line) continue;

        // 섹션 전환
        if (line === '[Header]') {
          section = 'HEADER';
          continue;
        }
        if (line === '[Data]') {
          section = 'DATA_WAIT_HEADER';
          continue;
        }

        // [Header] 섹션은 무시
        if (section === 'HEADER') {
          continue;
        }

        // [Data] 다음 줄 = 컬럼 헤더
        if (section === 'DATA_WAIT_HEADER') {
          dataHeader = rawLine.split('\t').map(s => s.trim());
          if (dataHeader.length < 2) {
            throw new Error('[Data] 헤더 라인이 비정상입니다. (탭 구분 여부 확인 필요)');
          }

          // 컬럼 인덱스 계산 및 캐시
          columnIndices = {
            snpNameIdx: dataHeader.indexOf(COLUMN_MAPPING.SNP_NAME),
            sampleIdIdx: dataHeader.indexOf(COLUMN_MAPPING.SAMPLE_ID),
            allele1Idx: dataHeader.indexOf(COLUMN_MAPPING.ALLELE1),
            allele2Idx: dataHeader.indexOf(COLUMN_MAPPING.ALLELE2),
            chrIdx: dataHeader.indexOf(COLUMN_MAPPING.CHR),
            positionIdx: dataHeader.indexOf(COLUMN_MAPPING.POSITION),
            snpIdx: dataHeader.indexOf(COLUMN_MAPPING.SNP),
          };

          if (columnIndices.snpNameIdx === -1 || columnIndices.sampleIdIdx === -1) {
            throw new Error(`필수 컬럼이 없습니다. (SNP Name: ${columnIndices.snpNameIdx}, Sample ID: ${columnIndices.sampleIdIdx})`);
          }

          this.logger.log(`[배치업로드] Data 헤더 컬럼 수: ${dataHeader.length}`);
          this.logger.log(`[배치업로드] 컬럼 인덱스 - SNP Name: ${columnIndices.snpNameIdx}, Sample ID: ${columnIndices.sampleIdIdx}, Chr: ${columnIndices.chrIdx}, Position: ${columnIndices.positionIdx}, SNP: ${columnIndices.snpIdx}`);

          section = 'DATA';
          continue;
        }

        // 데이터 라인 처리
        if (section === 'DATA') {
          if (!dataHeader || !columnIndices) {
            throw new Error('dataHeader 또는 columnIndices가 초기화되지 않았습니다.');
          }

          const values = rawLine.split('\t');
          
          // 컬럼 수 불일치 시 스킵
          if (values.length !== dataHeader.length) {
            this.logger.warn(
              `[배치업로드] 컬럼 수 불일치: header=${dataHeader.length}, values=${values.length} / line=${rawLine.slice(0, 120)}...`,
            );
            skippedCount++;
            continue;
          }

          // 필수 필드 검증
          const cowId = values[columnIndices.sampleIdIdx]?.trim();
          const snpName = values[columnIndices.snpNameIdx]?.trim();

          if (!cowId || !snpName) {
            this.logger.warn(`[배치업로드] 필수 필드 누락: cowId=${cowId}, snpName=${snpName}`);
            skippedCount++;
            continue;
          }

          // 배치 버퍼에 추가
          batchBuffer.push({
            cowId,
            snpName,
            chromosome: columnIndices.chrIdx !== -1 ? (values[columnIndices.chrIdx]?.trim() || null) : null,
            position: columnIndices.positionIdx !== -1 ? (values[columnIndices.positionIdx]?.trim() || null) : null,
            snpType: columnIndices.snpIdx !== -1 ? (values[columnIndices.snpIdx]?.trim() || null) : null,
            allele1: columnIndices.allele1Idx !== -1 ? (values[columnIndices.allele1Idx]?.trim() || null) : null,
            allele2: columnIndices.allele2Idx !== -1 ? (values[columnIndices.allele2Idx]?.trim() || null) : null,
          });

          totalRows++;

          // 배치 크기에 도달하면 즉시 DB에 저장
          if (batchBuffer.length >= BATCH_SIZE) {
            await flushBatch();
          }
        }
      }

      // 마지막 남은 배치 처리
      await flushBatch();

      // ============================================
      // 3단계: 결과 로깅
      // ============================================
      this.logger.log(`[배치업로드] 처리 완료`);
      this.logger.log(`[배치업로드] 총 처리: ${totalRows}건, 성공: ${successCount}건, 실패: ${errorCount}건, 스킵: ${skippedCount}건`);

      return {
        success: true,
        total: totalRows,
        successCount: successCount,
        errorCount: errorCount,
        skippedCount: skippedCount,
      };
    } catch (error) {
      this.logger.error(`[배치업로드] 처리 중 오류 발생: ${error.message}`);
      this.logger.error(error.stack);
      throw error;
    }
  }




  /**
   * 혈액화학검사 결과 배치 삽입
   * @param file - 파일
   * @returns 성공 여부
   */
  async batchInsertMptResult(file: Express.Multer.File) {
    this.logger.log(`[배치업로드] 혈액화학검사 결과 파일 처리 시작: ${file.originalname}`);

    try {
      // ============================================
      // 1단계: 엑셀파일 로드
      // ============================================
      this.logger.log('[배치업로드] 1단계: 엑셀 파일 로드 중...');
      const rawData = this.excelUtil.parseExcelData(file);

      if (rawData.length < 6) {
        throw new Error('엑셀 파일에 데이터가 부족합니다. (최소 6행 필요: 헤더 5행 + 데이터 1행)');
      }

      // ============================================
      // 2단계: 데이터 파싱 및 검증
      // ============================================
      this.logger.log('[배치업로드] 2단계: 데이터 파싱 중...');
      
      // MptDto를 기반으로 하되, 파싱 단계에서는 null 허용
      const mptDataArray: Array<Partial<MptDto> & { 
        cowId: string;
        fkFarmNo: number | null;
      }> = [];

      // 5행부터 데이터 시작 (인덱스 5)
      for (let rowIdx = 5; rowIdx < rawData.length; rowIdx++) {
        const row = rawData[rowIdx];

        // cowId 검증 (A열, 인덱스 0)
        const cowId = row[0];
        if (!cowId || typeof cowId !== 'string' || !cowId.trim()) {
          this.logger.warn(`[배치업로드] ${rowIdx + 1}행: cowId가 없어 건너뜀`);
          continue;
        }

        const trimmedCowId = cowId.trim();

        // cowShortNo 추출 (길이 검증)
        let cowShortNo: string;
        if (trimmedCowId.length >= 11) {
          cowShortNo = trimmedCowId.slice(7, 11);
        } else {
          this.logger.warn(`[배치업로드] ${rowIdx + 1}행: cowId 길이가 부족하여 cowShortNo 추출 실패 (cowId: ${trimmedCowId})`);
          cowShortNo = trimmedCowId.slice(-4) || trimmedCowId; // 최소한 뒤 4자리 또는 전체
        }

        // 날짜 파싱 (C열, 인덱스 2)
        const testDt = parseDate(row[2]);

        // 숫자 필드 파싱
        const monthAge = parseNumber(row[3]);
        const milkYield = parseNumber(row[4]);
        const parity = parseNumber(row[5]);
        const glucose = parseNumber(row[6]);
        const cholesterol = parseNumber(row[7]);
        const nefa = parseNumber(row[8]);
        const bcs = parseNumber(row[9]);
        const totalProtein = parseNumber(row[10]);
        const albumin = parseNumber(row[11]);
        const globulin = parseNumber(row[12]);
        const agRatio = parseNumber(row[13]);
        const bun = parseNumber(row[14]);
        const ast = parseNumber(row[15]);
        const ggt = parseNumber(row[16]);
        const fattyLiverIdx = parseNumber(row[17]);
        const calcium = parseNumber(row[18]);
        const phosphorus = parseNumber(row[19]);
        const caPRatio = parseNumber(row[20]);
        const magnesium = parseNumber(row[21]);
        const creatine = parseNumber(row[22]);

        mptDataArray.push({
          cowId: `KOR${trimmedCowId}`,
          cowShortNo,
          fkFarmNo: null, // 3단계에서 채움
          testDt,
          monthAge,
          milkYield,
          parity,
          glucose,
          cholesterol,
          nefa,
          bcs,
          totalProtein,
          albumin,
          globulin,
          agRatio,
          bun,
          ast,
          ggt,
          fattyLiverIdx,
          calcium,
          phosphorus,
          caPRatio,
          magnesium,
          creatine,
        });
      }

      this.logger.log(`[배치업로드] 파싱 완료: ${mptDataArray.length}건`);

      // ============================================
      // 3단계: cowId별 fkFarmNo 조회
      // ============================================
      this.logger.log('[배치업로드] 3단계: fkFarmNo 조회 중...');

      // 고유한 cowId 목록 추출
      const uniqueCowIds = [...new Set(mptDataArray.map(item => item.cowId))];
      this.logger.log(`[배치업로드] 고유 cowId 개수: ${uniqueCowIds.length}개`);

      // cowId별 fkFarmNo 매핑 생성 (일괄 조회로 성능 최적화)
      const cowIdToFarmNoMap = new Map<string, number | null>();

      if (uniqueCowIds.length > 0) {
        try {
          // 모든 cowId를 한 번에 조회 (IN 쿼리)
          const cows = await this.cowRepository.find({
            where: {
              cowId: In(uniqueCowIds),
              delDt: IsNull(),
            },
          });

          // 조회된 결과를 Map으로 변환
          const foundCowIdSet = new Set<string>();
          for (const cow of cows) {
            if (cow.cowId) {
              cowIdToFarmNoMap.set(cow.cowId, cow.fkFarmNo || null);
              foundCowIdSet.add(cow.cowId);
            }
          }

          // 조회되지 않은 cowId는 null로 설정
          for (const cowId of uniqueCowIds) {
            if (!foundCowIdSet.has(cowId)) {
              this.logger.warn(`[배치업로드] cowId "${cowId}"에 해당하는 개체를 찾을 수 없습니다.`);
              cowIdToFarmNoMap.set(cowId, null);
            }
          }

          this.logger.log(`[배치업로드] 조회 성공: ${cows.length}/${uniqueCowIds.length}개`);
        } catch (error) {
          this.logger.error(`[배치업로드] cowId 일괄 조회 중 오류: ${error.message}`);
          // 에러 발생 시 모든 cowId를 null로 설정
          for (const cowId of uniqueCowIds) {
            cowIdToFarmNoMap.set(cowId, null);
          }
        }
      }

      // fkFarmNo 추가
      const mptDataWithFarmNo = mptDataArray.map(item => ({
        ...item,
        fkFarmNo: cowIdToFarmNoMap.get(item.cowId) || null,
      }));

      // fkFarmNo가 있는 유효한 데이터만 필터링
      const validMptData = mptDataWithFarmNo.filter(item => item.fkFarmNo !== null);
      const invalidCount = mptDataWithFarmNo.length - validMptData.length;

      if (invalidCount > 0) {
        this.logger.warn(`[배치업로드] fkFarmNo가 없는 데이터 ${invalidCount}건 제외`);
      }

      this.logger.log(`[배치업로드] 유효한 MPT 데이터: ${validMptData.length}건`);

      // ============================================
      // 4단계: 데이터 DB Insert (Upsert)
      // ============================================
      this.logger.log('[배치업로드] 4단계: DB 배치 삽입(upsert) 중...');

      let successCount = 0;
      let errorCount = 0;

      // 배치 크기 설정 (한 번에 처리할 데이터 수)
      const BATCH_SIZE = 100;

      for (let i = 0; i < validMptData.length; i += BATCH_SIZE) {
        const batch = validMptData.slice(i, i + BATCH_SIZE);

        try {
          // 각 배치를 upsert 처리
          const insertPromises = batch.map(async (item) => {
            try {
              // 기존 데이터 조회 (cowId와 testDt 기준)
              const existing = await this.mptRepository.findOne({
                where: {
                  cowId: item.cowId,
                  testDt: item.testDt,
                  delDt: IsNull(),
                },
              });

              if (existing) {
                // 업데이트
                existing.fkFarmNo = item.fkFarmNo!;
                existing.cowShortNo = item.cowShortNo;
                existing.monthAge = item.monthAge;
                existing.milkYield = item.milkYield;
                existing.parity = item.parity;
                existing.glucose = item.glucose;
                existing.cholesterol = item.cholesterol;
                existing.nefa = item.nefa;
                existing.bcs = item.bcs;
                existing.totalProtein = item.totalProtein;
                existing.albumin = item.albumin;
                existing.globulin = item.globulin;
                existing.agRatio = item.agRatio;
                existing.bun = item.bun;
                existing.ast = item.ast;
                existing.ggt = item.ggt;
                existing.fattyLiverIdx = item.fattyLiverIdx;
                existing.calcium = item.calcium;
                existing.phosphorus = item.phosphorus;
                existing.caPRatio = item.caPRatio;
                existing.magnesium = item.magnesium;
                existing.creatine = item.creatine;
                await this.mptRepository.save(existing);
              } else {
                // 삽입
                const newMpt = this.mptRepository.create({
                  cowId: item.cowId,
                  cowShortNo: item.cowShortNo,
                  fkFarmNo: item.fkFarmNo!,
                  testDt: item.testDt,
                  monthAge: item.monthAge,
                  milkYield: item.milkYield,
                  parity: item.parity,
                  glucose: item.glucose,
                  cholesterol: item.cholesterol,
                  nefa: item.nefa,
                  bcs: item.bcs,
                  totalProtein: item.totalProtein,
                  albumin: item.albumin,
                  globulin: item.globulin,
                  agRatio: item.agRatio,
                  bun: item.bun,
                  ast: item.ast,
                  ggt: item.ggt,
                  fattyLiverIdx: item.fattyLiverIdx,
                  calcium: item.calcium,
                  phosphorus: item.phosphorus,
                  caPRatio: item.caPRatio,
                  magnesium: item.magnesium,
                  creatine: item.creatine,
                });
                await this.mptRepository.save(newMpt);
              }
              return true;
            } catch (error) {
              this.logger.error(`[배치업로드] 데이터 저장 실패 (cowId: ${item.cowId}, ${error.message}`);
              return false;
            }
          });

          const results = await Promise.all(insertPromises);
          successCount += results.filter(r => r === true).length;
          errorCount += results.filter(r => r === false).length;

          this.logger.log(`[배치업로드] 진행률: ${Math.min(i + BATCH_SIZE, validMptData.length)}/${validMptData.length}`);
        } catch (error) {
          this.logger.error(`[배치업로드] 배치 처리 중 오류: ${error.message}`);
          errorCount += batch.length;
        }
      }

      // ============================================
      // 5단계: 결과 로깅
      // ============================================
      this.logger.log(`[배치업로드] 처리 완료`);
      this.logger.log(`[배치업로드] 성공: ${successCount}건, 실패: ${errorCount}건, 제외: ${invalidCount}건`);

      return {
        success: true,
        total: mptDataArray.length,
        valid: validMptData.length,
        successCount: successCount,
        errorCount: errorCount,
        excludedCount: invalidCount,
      };
    } catch (error) {
      this.logger.error(`[배치업로드] 처리 중 오류 발생: ${error.message}`);
      this.logger.error(error.stack);
      throw error;
    }
  }

  
}