package pso

import (
	"errors"
	"fmt"
	"path/filepath"
	"strings"
	"system-altrak/internal/domain"
	"system-altrak/pkg/utils"
	"time"

	"github.com/shakinm/xlsReader/xls"
	"github.com/xuri/excelize/v2"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// ErrUnsupportedXLS is returned when a .xls file is too large, corrupted, or otherwise unsupported.
var ErrUnsupportedXLS = errors.New("unsupported or corrupted .xls file")
type ImportExcelSummary struct {
	Processed int `json:"processed"`
	Inserted  int `json:"inserted"`
	Updated   int `json:"updated"`
	Skipped   int `json:"skipped"`
}

func loadExcelRows(filePath string) ([][]string, error) {
	switch strings.ToLower(filepath.Ext(filePath)) {
	case ".xlsx":
		return loadXLSXRows(filePath)
	case ".xls":
		return loadXLSRows(filePath)
	case ".pdf":
		return loadPDFRows(filePath)
	default:
		return nil, fmt.Errorf("unsupported import format: %s", filepath.Ext(filePath))
	}
}

// defaultMinImportCols is the baseline minimum number of columns for a PSO import row.
// Setelah header terdeteksi, nilai ini di-override oleh requiredCols yang dihitung
// secara dinamis berdasarkan indeks kolom tertinggi (remIdx, daysIdx, dll).
// Ini memastikan bahwa meskipun kolom Remark berada di indeks 12+ (bukan 10),
// setiap baris tetap di-pad dengan benar.
const defaultMinImportCols = 11

// padRowToMinCols memastikan slice baris memiliki minimal n elemen.
// Ini mencegah kolom terakhir (Remark) hilang karena library Excel
// mengembalikan slice dengan panjang berbeda-beda per baris.
func padRowToMinCols(row []string, n int) []string {
	if len(row) >= n {
		return row
	}
	padded := make([]string, n)
	copy(padded, row)
	return padded
}

func loadXLSXRows(filePath string) ([][]string, error) {
	book, err := excelize.OpenFile(filePath)
	if err != nil {
		return nil, err
	}
	defer book.Close()

	sheets := book.GetSheetList()
	if len(sheets) == 0 {
		return [][]string{}, nil
	}

	rows, err := book.GetRows(sheets[0], excelize.Options{RawCellValue: true})
	if err != nil {
		return nil, err
	}

	// Pad setiap baris agar kolom Remark (kolom terakhir) tidak terpotong.
	// excelize.GetRows() mengembalikan slice per baris dengan panjang berbeda
	// tergantung data: jika sel terakhir kosong, slice-nya lebih pendek.
	for i := range rows {
		rows[i] = padRowToMinCols(rows[i], defaultMinImportCols)
	}

	return rows, nil
}

func loadXLSRows(filePath string) (rows [][]string, err error) {
	book, err := xls.OpenFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to open .xls file: %w", err)
	}

	if book.GetNumberSheets() == 0 {
		return [][]string{}, nil
	}

	sheet, err := book.GetSheet(0)
	if err != nil {
		return nil, fmt.Errorf("failed to get sheet: %w", err)
	}

	numRows := sheet.GetNumberRows()
	if numRows > 65535 {
		return nil, ErrUnsupportedXLS
	}

	rows = make([][]string, 0, numRows)
	for rowIndex := 0; rowIndex <= numRows; rowIndex++ {
		row, err := sheet.GetRow(rowIndex)
		if err != nil {
			rows = append(rows, []string{})
			continue
		}

		cols := row.GetCols()
		if len(cols) == 0 {
			rows = append(rows, []string{})
			continue
		}

		lastCol := len(cols)
		if lastCol > 256 {
			lastCol = 256
		}

		values := make([]string, lastCol)
		for colIndex := 0; colIndex < lastCol; colIndex++ {
			cell, err := row.GetCol(colIndex)
			if err == nil && cell != nil {
				values[colIndex] = strings.TrimSpace(cell.GetString())
			} else {
				values[colIndex] = ""
			}
		}
		// Pad baris agar kolom Remark (indeks 10) selalu ada.
		// JANGAN gunakan trimTrailingEmptyCells karena akan memotong
		// kolom Remark jika kebetulan kolom terakhir kosong.
		rows = append(rows, padRowToMinCols(values, defaultMinImportCols))
	}

	return rows, nil
}

func looksLikePSONumber(value string) bool {
	normalized := strings.TrimSpace(value)
	if normalized == "" {
		return false
	}
	
	// Only accept PSO numbers starting with "SA" (case-insensitive)
	if !strings.HasPrefix(strings.ToUpper(normalized), "SA") {
		return false
	}
	
	// If it contains a comma or dot but no letters, it's likely an amount or total row
	if (strings.Contains(normalized, ",") || strings.Contains(normalized, ".")) && 
	   !strings.ContainsAny(strings.ToLower(normalized), "abcdefghijklmnopqrstuvwxyz") {
		return false
	}

	// Must contain at least one digit
	return strings.ContainsAny(normalized, "0123456789")
}

func normalizeImportHeaderText(value string) string {
	normalized := strings.ToLower(strings.TrimSpace(value))
	normalized = strings.ReplaceAll(normalized, ".", "")
	normalized = strings.ReplaceAll(normalized, "/", "")
	normalized = strings.ReplaceAll(normalized, "-", "")
	normalized = strings.ReplaceAll(normalized, " ", "")
	return normalized
}

func (s *serviceImpl) importExcelRowsWithRepo(repo PSORepository, rows [][]string, branchID uint, jobID uint) ImportExcelSummary {
	summary := ImportExcelSummary{}	// Struktur nyata file PSO Outstanding:
	//   Baris 0: "PSO OUTSTANDING REPORT -ALL -By SalesPrice"  ← judul
	//   Baris 1: "1st Outsd Date: 01/01/2009  Cutoff Date: 04/02/2026" ← meta
	//   Baris 2: "04/02/2026"  ← sub-meta
	//   Baris 3: "N~ │ PSO No. │ Date │ PO No. │ Customer │ TOP │ Disc │ │ Amount │ Days │ Remark"
	//   Baris 4+: data
	//
	// Aturan: Baris header harus mengandung MINIMAL 3 keyword kolom data
	// (bukan hanya 2), dan minimal ada kolom PSO/PO supaya baris meta
	// yang kebetulan mengandung "date" tidak terdeteksi sebagai header.
	// ─────────────────────────────────────────────────────────────────────────
	psoIdx, dateIdx, poIdx, custIdx := -1, -1, -1, -1
	topIdx, discIdx, curIdx, amtIdx, daysIdx, remIdx := -1, -1, -1, -1, -1, -1
	dataStartRow := -1

	maxHeadScan := 30
	if len(rows) < maxHeadScan {
		maxHeadScan = len(rows)
	}

	for i := 0; i < maxHeadScan; i++ {
		row := rows[i]
		if len(row) < 3 {
			// Baris dengan <3 sel tidak mungkin baris header tabel
			continue
		}

		localPso, localDate, localPo, localCust := -1, -1, -1, -1
		localTop, localDisc, localCur, localAmt, localDays, localRem := -1, -1, -1, -1, -1, -1
		kwFound := 0

		for j, cell := range row {
			c := normalizeImportHeaderText(cell)
			if c == "" {
				continue
			}

			// ── PSO No. ──────────────────────────────────────────────────────
			// Header file: "PSO No." → normalized: "psono"
			if localPso == -1 && (c == "psono" || c == "nopso" || c == "pso" ||
				strings.HasPrefix(c, "psono") || strings.HasSuffix(c, "psono") || strings.Contains(c, "pso")) {
				localPso = j
				kwFound++
			}

			// ── Date ─────────────────────────────────────────────────────────
			// PENTING: Hanya cocokkan jika c mengandung "date", "tgl", "tanggal"
			// dan bukan metadata luar
			if localDate == -1 &&
				(c == "date" || c == "tgl" || c == "tanggal" ||
					c == "psodate" || c == "tglpso" || c == "tanggalpso" ||
					strings.Contains(c, "tgl") || strings.Contains(c, "date")) &&
				c != "cutoffdate" && c != "1stoutsddate" && !strings.Contains(c, "cutoff") {
				localDate = j
				kwFound++
			}

			// ── PO No. ───────────────────────────────────────────────────────
			// Header file: "PO No." → normalized: "pono"
			if localPo == -1 && (c == "pono" || c == "nopo" || c == "po" ||
				strings.HasPrefix(c, "pono") || strings.HasSuffix(c, "pono") || strings.Contains(c, "po")) {
				localPo = j
				kwFound++
			}

			// ── Customer ─────────────────────────────────────────────────────
			if localCust == -1 && (strings.HasPrefix(c, "customer") || strings.Contains(c, "customer") || c == "pelanggan" || c == "cust") {
				localCust = j
				kwFound++
			}

			// ── Amount ───────────────────────────────────────────────────────
			if localAmt == -1 && (strings.HasPrefix(c, "amount") || strings.Contains(c, "amount") || c == "jumlah" || c == "nilai" || c == "amt") {
				localAmt = j
			}

			// ── TOP ──────────────────────────────────────────────────────────
			if localTop == -1 && (c == "top" || strings.Contains(c, "term") || strings.Contains(c, "payment")) {
				localTop = j
			}

			// ── Disc ─────────────────────────────────────────────────────────
			if localDisc == -1 && (strings.HasPrefix(c, "disc") || strings.Contains(c, "disc") || c == "diskon" || c == "dc") {
				localDisc = j
			}

			// ── Days ─────────────────────────────────────────────────────────
			if localDays == -1 && (c == "days" || strings.Contains(c, "days") || c == "hari" || strings.Contains(c, "umur")) {
				localDays = j
			}

			// ── Remark ───────────────────────────────────────────────────────
			if localRem == -1 && (strings.Contains(c, "remark") || strings.Contains(c, "keterangan") || strings.HasPrefix(c, "ket") || strings.Contains(c, "note") || c == "rem") {
				localRem = j
			}

			// ── Currency ─────────────────────────────────────────────────────
			if localCur == -1 && (strings.HasPrefix(c, "currency") || strings.Contains(c, "currency") || c == "cur" || c == "curr" || c == "mata") {
				localCur = j
			}
		}

		// Wajib menemukan minimal PSO + satu kolom lain (PO atau Customer atau Date)
		// agar tidak tersangkut di baris meta yang kebetulan mengandung satu keyword.
		if localPso != -1 && kwFound >= 2 {
			psoIdx = localPso
			dateIdx = localDate
			poIdx = localPo
			custIdx = localCust
			topIdx = localTop
			discIdx = localDisc
			curIdx = localCur
			amtIdx = localAmt
			daysIdx = localDays
			remIdx = localRem
			dataStartRow = i + 1

			// Fallback currency: jika tidak ada header "Currency",
			// cek apakah sel kosong di antara Disc dan Amount berisi "IDR"/"USD".
			// Jika tidak, set curIdx = amtIdx-1 hanya kalau masuk akal.
			if curIdx == -1 && amtIdx > 1 {
				curIdx = amtIdx - 1
			}

			// Koreksi Cerdas Kolom Remark:
			// Di beberapa file ekspor, kolom dengan label "Remark" berada di kolom L (indeks 11) tapi kosong,
			// sementara data teks remark asli berada di kolom K (indeks 10) yang headernya kosong.
			// Logika: Jika daysIdx terdeteksi, dan ada kolom kosong di antara daysIdx and remIdx,
			// maka kolom kosong tersebut (indeks remIdx - 1) adalah kolom remark yang sebenarnya.
			if remIdx > 0 && daysIdx != -1 && remIdx > daysIdx+1 {
				headerTextOfPrev := ""
				if remIdx-1 < len(row) {
					headerTextOfPrev = normalizeImportHeaderText(row[remIdx-1])
				}
				if headerTextOfPrev == "" {
					utils.Info("PSO Import: Mengoreksi kolom Remark ke kolom sebelah kiri yang kosong headernya",
						zap.Int("old_remIdx", remIdx),
						zap.Int("new_remIdx", remIdx-1),
					)
					remIdx = remIdx - 1
				}
			}
			break
		}
	}

	// ─────────────────────────────────────────────────────────────────────────
	// FALLBACK: Jika header sama sekali tidak terdeteksi, gunakan posisi kolom
	// yang sesuai dengan struktur file PSO Outstanding standar perusahaan:
	//   0:N  1:PSO No.  2:Date  3:PO No.  4:Customer  5:TOP  6:Disc  7:[cur]  8:Amount  9:Days  10:Remark
	// ─────────────────────────────────────────────────────────────────────────
	if dataStartRow == -1 {
		utils.Log.Warn("PSO Import: Baris header tidak terdeteksi, menggunakan mapping kolom default standar PSO Outstanding")
		psoIdx = 1  // Kolom B: PSO No.
		dateIdx = 2  // Kolom C: Date
		poIdx = 3    // Kolom D: PO No.
		custIdx = 4  // Kolom E: Customer
		topIdx = 5   // Kolom F: TOP
		discIdx = 6  // Kolom G: Disc
		curIdx = 7   // Kolom H: Currency (biasanya "IDR")
		amtIdx = 8   // Kolom I: Amount
		daysIdx = 9  // Kolom J: Days
		remIdx = 10  // Kolom K: Remark
		dataStartRow = 4 // Lewati 3 baris header/meta + 1 baris header tabel
	}

	// ─────────────────────────────────────────────────────────────────────────
	// PENTING: Hitung jumlah kolom yang BENAR-BENAR dibutuhkan berdasarkan
	// indeks kolom tertinggi yang terdeteksi dari header.
	// Ini mengatasi masalah di mana file Excel memiliki kolom kosong di tengah
	// (mis. Currency kosong antara Disc dan Amount), sehingga Remark bergeser
	// ke indeks 12+ alih-alih indeks 10 standar.
	// ─────────────────────────────────────────────────────────────────────────
	requiredCols := defaultMinImportCols
	for _, idx := range []int{psoIdx, dateIdx, poIdx, custIdx, topIdx, discIdx, curIdx, amtIdx, daysIdx, remIdx} {
		if idx >= requiredCols {
			requiredCols = idx + 1
		}
	}

	// Re-pad SEMUA baris (termasuk header dan data) ke requiredCols.
	// Ini kritis karena loadXLSXRows/loadXLSRows hanya mem-pad ke
	// defaultMinImportCols (11), tapi jika remIdx = 12 maka kita butuh
	// minimal 13 kolom agar safeCell(row, 12) tidak mengembalikan "".
	if requiredCols > defaultMinImportCols {
		for i := range rows {
			rows[i] = padRowToMinCols(rows[i], requiredCols)
		}
	}

	// ─────────────────────────────────────────────────────────────────────────
	// Log hasil deteksi untuk kemudahan debugging
	// ─────────────────────────────────────────────────────────────────────────
	utils.Info("PSO Import: Mapping kolom terdeteksi",
		zap.Int("dataStartRow", dataStartRow),
		zap.Int("psoIdx", psoIdx),
		zap.Int("dateIdx", dateIdx),
		zap.Int("poIdx", poIdx),
		zap.Int("custIdx", custIdx),
		zap.Int("topIdx", topIdx),
		zap.Int("discIdx", discIdx),
		zap.Int("curIdx", curIdx),
		zap.Int("amtIdx", amtIdx),
		zap.Int("daysIdx", daysIdx),
		zap.Int("remIdx", remIdx),
		zap.Int("requiredCols", requiredCols),
	)

	totalItems := len(rows) - dataStartRow
	if totalItems < 0 {
		totalItems = 0
	}

	if jobID > 0 && s.job != nil {
		_ = s.job.UpdateProgress(jobID, 0, fmt.Sprintf("Processing %d records...", totalItems))
	}

	// ─────────────────────────────────────────────────────────────────────────
	// FASE 2: Proses baris data
	// ─────────────────────────────────────────────────────────────────────────

	// Recover global agar satu baris yang korup tidak menghentikan seluruh proses
	defer func() {
		if r := recover(); r != nil {
			utils.Log.Error("PSO Import: Panic saat memproses baris data",
				zap.Any("panic", r),
				zap.Uint("job_id", jobID),
			)
			if jobID > 0 && s.job != nil {
				_ = s.job.FailJob(jobID, fmt.Sprintf("Critical Runtime Error: %v", r))
			}
		}
	}()

	// ── Pre-fetch dan Cache data yang sudah ada untuk optimasi performa ──
	existingCache := make(map[string]domain.OperationalRecord)
	if psoIdx >= 0 {
		var psoNos []string
		for i := dataStartRow; i < len(rows); i++ {
			row := rows[i]
			if len(row) > psoIdx {
				psoNo := safeCell(row, psoIdx)
				if looksLikePSONumber(psoNo) {
					psoNos = append(psoNos, psoNo)
				}
			}
		}

		if len(psoNos) > 0 {
			records, err := repo.GetRecordsByPsoNos(psoNos)
			if err == nil {
				for _, rec := range records {
					existingCache[rec.PsoNo] = rec
				}
				utils.Info("PSO Import: Batch cache inisialisasi sukses", zap.Int("cached_count", len(existingCache)))
			} else {
				utils.Log.Warn("PSO Import: gagal inisialisasi batch cache, fallback ke individual query", zap.Error(err))
			}
		}
	}

	for i := dataStartRow; i < len(rows); i++ {
		if jobID > 0 && s.job != nil && (i-dataStartRow)%50 == 0 {
			_ = s.job.UpdateProgress(jobID, i-dataStartRow, "")
		}

		row := rows[i]

		// Lewati baris kosong atau terlalu pendek
		if len(row) == 0 {
			continue
		}

		// Lewati jika kolom PSO tidak valid
		if psoIdx < 0 || psoIdx >= len(row) {
			summary.Skipped++
			continue
		}

		psoNo := safeCell(row, psoIdx)
		
		// Silent ignore for completely empty PSO column
		if psoNo == "" {
			hasData := false
			for _, cell := range row {
				if strings.TrimSpace(cell) != "" {
					hasData = true
					break
				}
			}
			if hasData {
				summary.Skipped++
			}
			continue
		}

		// Lewati baris sub-total, total, atau baris yang bukan data
		if !looksLikePSONumber(psoNo) {
			continue
		}

		// ── Parsing tanggal ───────────────────────────────────────────────────
		psoDateStr := strings.TrimSpace(safeCell(row, dateIdx))
		psoDate, _ := utils.ParseDate(psoDateStr)
		var psoDatePtr *time.Time
		if !psoDate.IsZero() {
			psoDatePtr = &psoDate
		}

		// ── Pad baris agar kolom Remark selalu bisa diakses ─────────────────
		row = padRowToMinCols(row, requiredCols)

		// ── Bangun record ─────────────────────────────────────────────────────
		remarkVal := safeCell(row, remIdx)

		// Info log (bukan Debug) untuk tracking masalah Remark —
		// hanya cetak 5 baris pertama agar tidak membanjiri log
		if remIdx >= 0 && (i-dataStartRow) < 5 {
			utils.Info("PSO Import: Remark extraction",
				zap.String("pso_no", psoNo),
				zap.Int("remIdx", remIdx),
				zap.Int("row_len", len(row)),
				zap.String("remark_value", remarkVal),
			)
		}

		record := domain.OperationalRecord{
			PsoNo:        psoNo,
			PsoDate:      psoDatePtr,
			PoNo:         safeCell(row, poIdx),
			CustomerName: safeCell(row, custIdx),
			Currency:     "IDR", // Default currency
			Remark:       remarkVal,
			BranchID:     branchID,
			RecordStatus: "IMPORT",
		}

		// ── Currency: ambil dari sel jika ada, jika tidak default IDR ─────────
		if curIdx >= 0 {
			curValue := strings.TrimSpace(safeCell(row, curIdx))
			// Hanya override jika nilainya seperti kode mata uang (2-5 huruf)
			if len(curValue) >= 2 && len(curValue) <= 5 {
				record.Currency = strings.ToUpper(curValue)
			}
		}

		// ── Numerik ───────────────────────────────────────────────────────────
		if topIdx >= 0 {
			record.TOP = int64(utils.ParseFloat(safeCell(row, topIdx)))
		}
		if discIdx >= 0 {
			record.Discount = utils.ParseFloat(safeCell(row, discIdx))
		}
		if daysIdx >= 0 {
			record.Days = int64(utils.ParseFloat(safeCell(row, daysIdx)))
		}
		if amtIdx >= 0 {
			// Amount disimpan dalam satuan sen (×100) untuk menghindari presisi float
			record.Amount = int64(utils.ParseFloat(safeCell(row, amtIdx)) * 100)
			record.AmountIDR = record.Amount
		}

		// ── Smart Merge Logic ────────────────────────────────────────────────
		var existing domain.OperationalRecord
		var isUpdate bool
		var lookupErr error

		if cachedRec, ok := existingCache[psoNo]; ok {
			existing = cachedRec
			isUpdate = true
		} else {
			// Fallback individual lookup jika tidak ada di cache
			dbRec, err := repo.GetRecordByPsoNo(psoNo)
			if err == nil && dbRec != nil {
				existing = *dbRec
				isUpdate = true
			} else {
				lookupErr = err
			}
		}
		
		if isUpdate {
			// Jika data sudah terverifikasi, kita sangat berhati-hati.
			// Secara profesional, data terverifikasi tidak boleh ditimpa impor masal
			// kecuali ada perubahan finansial yang signifikan atau admin memaksa.
			if existing.IsVerified {
				utils.Info("PSO Import: Melewati update karena record sudah terverifikasi", zap.String("pso_no", psoNo))
				summary.Skipped++
				continue
			}

			// Salin ID agar GORM tahu ini adalah update
			record.ID = existing.ID
			
			// Proteksi Keterangan (Remark): 
			// Kita anggap data lama 'lebih berharga' jika data baru kosong atau hanya sebagian kecil.
			newRem := strings.TrimSpace(record.Remark)
			oldRem := strings.TrimSpace(existing.Remark)
			if newRem == "" && oldRem != "" {
				record.Remark = existing.Remark
			} else if newRem != "" && oldRem != "" && newRem != oldRem {
				// Jika remark baru berbeda tapi lebih pendek signifikan dari yang lama, 
				// mungkin ini data 'potongan' dari export sistem lain yang kurang lengkap.
				// Kita simpan keduanya (gabungkan) agar tidak kehilangan jejak audit.
				if len(newRem) < len(oldRem)/2 && !strings.Contains(oldRem, newRem) {
					record.Remark = oldRem + " [Update: " + newRem + "]"
				}
			}

			// Proteksi PO No: Gunakan yang lama jika di Excel kosong
			if strings.TrimSpace(record.PoNo) == "" && existing.PoNo != "" {
				record.PoNo = existing.PoNo
			}

			// Proteksi Tanggal: Gunakan yang lama jika di Excel tidak valid
			if record.PsoDate == nil && existing.PsoDate != nil {
				record.PsoDate = existing.PsoDate
			}
			
			// Pertahankan status record yang lebih tinggi (misal: jika sudah diproses sebagian)
			if existing.RecordStatus != "" && existing.RecordStatus != "IMPORT" {
				record.RecordStatus = existing.RecordStatus
			}
		}

		if lookupErr != nil && !errors.Is(lookupErr, gorm.ErrRecordNotFound) {
			utils.Log.Warn("PSO Import: gagal lookup PSO No", zap.String("pso_no", psoNo), zap.Error(lookupErr))
			summary.Skipped++
			continue
		}

		var err error
		if isUpdate {
			err = repo.UpdateRecord(&record)
		} else {
			err = repo.UpsertRecord(&record)
		}

		if err != nil {
			utils.Log.Warn("PSO Import: gagal menyimpan record", zap.String("pso_no", psoNo), zap.Error(err))
			summary.Skipped++
			continue
		}

		// Update cache agar data terbaru tersedia untuk baris berikutnya yang mungkin menduplikasi
		existingCache[psoNo] = record

		summary.Processed++
		if isUpdate {
			summary.Updated++
		} else {
			summary.Inserted++
		}
	}

	return summary
}
