package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"
	"sort"
	"strconv"
	"strings"

	"github.com/xuri/excelize/v2"
	"gitlab.com/mbugroup/lti-api.git/internal/config"
	"gitlab.com/mbugroup/lti-api.git/internal/database"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gorm.io/gorm"
)

type importOptions struct {
	FilePath string
	Sheet    string
	Apply    bool
}

type headerIndexes struct {
	KandangID   int
	KandangName int
	HouseType   int
}

type kandangHouseTypeImportRow struct {
	RowNumber   int
	KandangID   uint
	KandangName string
	HouseType   string
}

type validationIssue struct {
	Row     int
	Field   string
	Message string
}

func (i validationIssue) Error() string {
	if i.Row > 0 {
		return fmt.Sprintf("row=%d field=%s message=%s", i.Row, i.Field, i.Message)
	}
	return fmt.Sprintf("field=%s message=%s", i.Field, i.Message)
}

type kandangResolver interface {
	ResolveActiveKandangs(ctx context.Context, kandangIDs []uint) (map[uint]string, error)
}

type dbKandangResolver struct {
	db *gorm.DB
}

type txRunner interface {
	InTx(ctx context.Context, fn func(store kandangHouseTypeStore) error) error
}

type dbTxRunner struct {
	db *gorm.DB
}

type kandangHouseTypeStore interface {
	UpdateKandangHouseType(ctx context.Context, kandangID uint, houseType string) (bool, error)
	NormalizeNullHouseType(ctx context.Context) (int64, error)
}

type dbKandangHouseTypeStore struct {
	db *gorm.DB
}

type kandangIdentityRow struct {
	ID   uint   `gorm:"column:id"`
	Name string `gorm:"column:name"`
}

type applyRowResult struct {
	RowNumber int
	KandangID uint
	HouseType string
	Changed   bool
}

func main() {
	var opts importOptions
	flag.StringVar(&opts.FilePath, "file", "", "Path to .xlsx file (required)")
	flag.StringVar(&opts.Sheet, "sheet", "", "Sheet name (optional, default: first sheet)")
	flag.BoolVar(&opts.Apply, "apply", false, "Apply changes. If false, run as dry-run")
	flag.Parse()

	opts.FilePath = strings.TrimSpace(opts.FilePath)
	opts.Sheet = strings.TrimSpace(opts.Sheet)

	if opts.FilePath == "" {
		log.Fatal("--file is required")
	}

	sheetName, rows, parseIssues, err := parseKandangHouseTypeFile(opts.FilePath, opts.Sheet)
	if err != nil {
		log.Fatalf("failed reading excel: %v", err)
	}

	ctx := context.Background()
	db := database.Connect(config.DBHost, config.DBName)
	resolver := dbKandangResolver{db: db}

	kandangNameByID, err := resolver.ResolveActiveKandangs(ctx, collectKandangIDs(rows))
	if err != nil {
		log.Fatalf("failed validating kandang_id against kandangs: %v", err)
	}

	issues := append([]validationIssue{}, parseIssues...)
	issues = append(issues, buildMissingKandangIssues(rows, kandangNameByID)...)
	issues = append(issues, buildNameMismatchIssues(rows, kandangNameByID)...)
	sortValidationIssues(issues)

	fmt.Printf("Mode: %s\n", modeLabel(opts.Apply))
	fmt.Printf("File: %s\n", opts.FilePath)
	fmt.Printf("Sheet: %s\n", sheetName)
	fmt.Printf("Rows parsed: %d\n", len(rows))
	fmt.Printf("Rows invalid: %d\n", len(issues))
	fmt.Println()

	if len(rows) > 0 {
		printPlanRows(rows, kandangNameByID)
		fmt.Println()
	}

	if len(issues) > 0 {
		fmt.Println("Validation errors:")
		for _, issue := range issues {
			fmt.Printf("ERROR %s\n", issue.Error())
		}
		fmt.Println()
		fmt.Printf("Summary: planned=%d applied=0 normalized_null_to_open_house=0 failed=%d\n", len(rows), len(issues))
		os.Exit(1)
	}

	if !opts.Apply {
		fmt.Printf("Summary: planned=%d applied=0 normalized_null_to_open_house=0 failed=0\n", len(rows))
		return
	}

	rowResults, normalizedCount, err := applyImportRows(ctx, dbTxRunner{db: db}, rows)
	if err != nil {
		log.Fatalf("apply failed: %v", err)
	}

	for _, result := range rowResults {
		fmt.Printf(
			"DONE row=%d kandang_id=%d house_type=%s status=%s\n",
			result.RowNumber,
			result.KandangID,
			result.HouseType,
			applyStatus(result.Changed),
		)
	}

	appliedCount := countChangedRows(rowResults)
	fmt.Println()
	fmt.Printf(
		"Summary: planned=%d applied=%d normalized_null_to_open_house=%d failed=0\n",
		len(rows),
		appliedCount,
		normalizedCount,
	)
}

func parseKandangHouseTypeFile(
	filePath string,
	requestedSheet string,
) (string, []kandangHouseTypeImportRow, []validationIssue, error) {
	workbook, err := excelize.OpenFile(filePath)
	if err != nil {
		return "", nil, nil, err
	}
	defer func() {
		_ = workbook.Close()
	}()

	sheetName, err := resolveSheetName(workbook, requestedSheet)
	if err != nil {
		return "", nil, nil, err
	}

	allRows, err := workbook.GetRows(sheetName, excelize.Options{RawCellValue: true})
	if err != nil {
		return "", nil, nil, err
	}
	if len(allRows) == 0 {
		return sheetName, nil, []validationIssue{{Field: "header", Message: "sheet is empty"}}, nil
	}

	indexes, headerIssues := parseHeaderIndexes(allRows[0])
	if len(headerIssues) > 0 {
		return sheetName, nil, headerIssues, nil
	}

	rows := make([]kandangHouseTypeImportRow, 0, len(allRows)-1)
	issues := make([]validationIssue, 0)
	seenKandangIDs := make(map[uint]int)

	for idx := 1; idx < len(allRows); idx++ {
		rowNumber := idx + 1
		rawRow := allRows[idx]

		if isRowEmpty(rawRow) {
			continue
		}

		parsed, rowIssues := parseDataRow(rawRow, rowNumber, indexes, seenKandangIDs)
		if len(rowIssues) > 0 {
			issues = append(issues, rowIssues...)
			continue
		}

		rows = append(rows, *parsed)
	}

	if len(rows) == 0 && len(issues) == 0 {
		issues = append(issues, validationIssue{Field: "rows", Message: "no data rows found"})
	}

	return sheetName, rows, issues, nil
}

func resolveSheetName(workbook *excelize.File, requestedSheet string) (string, error) {
	if workbook == nil {
		return "", fmt.Errorf("workbook is nil")
	}

	sheets := workbook.GetSheetList()
	if len(sheets) == 0 {
		return "", fmt.Errorf("workbook has no sheets")
	}

	if requestedSheet == "" {
		return sheets[0], nil
	}

	for _, sheet := range sheets {
		if strings.EqualFold(strings.TrimSpace(sheet), strings.TrimSpace(requestedSheet)) {
			return sheet, nil
		}
	}

	return "", fmt.Errorf("sheet %q not found", requestedSheet)
}

func parseHeaderIndexes(headerRow []string) (headerIndexes, []validationIssue) {
	indexes := headerIndexes{KandangID: -1, KandangName: -1, HouseType: -1}
	issues := make([]validationIssue, 0)

	for idx, raw := range headerRow {
		header := normalizeHeader(raw)
		if header == "" {
			continue
		}

		switch header {
		case "kandang_id":
			if indexes.KandangID >= 0 {
				issues = append(issues, validationIssue{Field: "header", Message: "duplicate header kandang_id"})
			}
			indexes.KandangID = idx
		case "kandang_name":
			if indexes.KandangName >= 0 {
				issues = append(issues, validationIssue{Field: "header", Message: "duplicate header kandang_name"})
			}
			indexes.KandangName = idx
		case "house_type", "type_house":
			if indexes.HouseType >= 0 {
				issues = append(issues, validationIssue{Field: "header", Message: "duplicate header house_type"})
			}
			indexes.HouseType = idx
		}
	}

	if indexes.KandangID < 0 {
		issues = append(issues, validationIssue{Field: "kandang_id", Message: "required header is missing"})
	}
	if indexes.KandangName < 0 {
		issues = append(issues, validationIssue{Field: "kandang_name", Message: "required header is missing"})
	}
	if indexes.HouseType < 0 {
		issues = append(issues, validationIssue{Field: "house_type", Message: "required header is missing"})
	}

	return indexes, issues
}

func parseDataRow(
	rawRow []string,
	rowNumber int,
	indexes headerIndexes,
	seenKandangIDs map[uint]int,
) (*kandangHouseTypeImportRow, []validationIssue) {
	issues := make([]validationIssue, 0)

	kandangIDRaw := strings.TrimSpace(cellValue(rawRow, indexes.KandangID))
	kandangID, err := parsePositiveUint(kandangIDRaw)
	if err != nil {
		issues = append(issues, validationIssue{Row: rowNumber, Field: "kandang_id", Message: err.Error()})
	}

	kandangNameRaw := strings.TrimSpace(cellValue(rawRow, indexes.KandangName))
	if kandangNameRaw == "" {
		issues = append(issues, validationIssue{Row: rowNumber, Field: "kandang_name", Message: "is required"})
	}

	houseTypeRaw := strings.TrimSpace(cellValue(rawRow, indexes.HouseType))
	houseType, err := normalizeHouseType(houseTypeRaw)
	if err != nil {
		issues = append(issues, validationIssue{Row: rowNumber, Field: "house_type", Message: err.Error()})
	}

	if kandangID > 0 {
		if previousRow, exists := seenKandangIDs[kandangID]; exists {
			issues = append(issues, validationIssue{
				Row:     rowNumber,
				Field:   "kandang_id",
				Message: fmt.Sprintf("duplicate value %d (already used in row %d)", kandangID, previousRow),
			})
		} else {
			seenKandangIDs[kandangID] = rowNumber
		}
	}

	if len(issues) > 0 {
		return nil, issues
	}

	return &kandangHouseTypeImportRow{
		RowNumber:   rowNumber,
		KandangID:   kandangID,
		KandangName: kandangNameRaw,
		HouseType:   houseType,
	}, nil
}

func normalizeHouseType(raw string) (string, error) {
	normalized := strings.ToLower(strings.TrimSpace(raw))
	if normalized == "" {
		return string(utils.HouseTypeOpenHouse), nil
	}

	switch normalized {
	case string(utils.HouseTypeOpenHouse), string(utils.HouseTypeCloseHouse):
		return normalized, nil
	default:
		return "", fmt.Errorf("must be one of: open_house, close_house (or empty for default open_house)")
	}
}

func parsePositiveUint(raw string) (uint, error) {
	if raw == "" {
		return 0, fmt.Errorf("is required")
	}

	uintValue, err := strconv.ParseUint(raw, 10, 64)
	if err == nil {
		if uintValue == 0 {
			return 0, fmt.Errorf("must be greater than 0")
		}
		return uint(uintValue), nil
	}

	floatValue, floatErr := strconv.ParseFloat(raw, 64)
	if floatErr != nil {
		return 0, fmt.Errorf("must be a positive integer")
	}
	if floatValue <= 0 {
		return 0, fmt.Errorf("must be greater than 0")
	}
	if floatValue != float64(uint(floatValue)) {
		return 0, fmt.Errorf("must be a positive integer")
	}

	return uint(floatValue), nil
}

func isRowEmpty(row []string) bool {
	for _, cell := range row {
		if strings.TrimSpace(cell) != "" {
			return false
		}
	}
	return true
}

func normalizeHeader(raw string) string {
	return strings.ToLower(strings.TrimSpace(raw))
}

func cellValue(row []string, index int) string {
	if index < 0 || index >= len(row) {
		return ""
	}
	return row[index]
}

func collectKandangIDs(rows []kandangHouseTypeImportRow) []uint {
	ids := make([]uint, 0, len(rows))
	seen := make(map[uint]struct{}, len(rows))
	for _, row := range rows {
		if row.KandangID == 0 {
			continue
		}
		if _, exists := seen[row.KandangID]; exists {
			continue
		}
		seen[row.KandangID] = struct{}{}
		ids = append(ids, row.KandangID)
	}
	sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
	return ids
}

func (r dbKandangResolver) ResolveActiveKandangs(ctx context.Context, kandangIDs []uint) (map[uint]string, error) {
	result := make(map[uint]string)
	if len(kandangIDs) == 0 {
		return result, nil
	}

	rows := make([]kandangIdentityRow, 0, len(kandangIDs))
	if err := r.db.WithContext(ctx).
		Table("kandangs").
		Select("id, name").
		Where("id IN ?", kandangIDs).
		Where("deleted_at IS NULL").
		Scan(&rows).Error; err != nil {
		return nil, err
	}

	for _, row := range rows {
		result[row.ID] = row.Name
	}

	return result, nil
}

func buildMissingKandangIssues(rows []kandangHouseTypeImportRow, kandangNameByID map[uint]string) []validationIssue {
	issues := make([]validationIssue, 0)
	for _, row := range rows {
		if _, exists := kandangNameByID[row.KandangID]; exists {
			continue
		}
		issues = append(issues, validationIssue{
			Row:     row.RowNumber,
			Field:   "kandang_id",
			Message: fmt.Sprintf("value %d must reference an active kandang", row.KandangID),
		})
	}
	return issues
}

func buildNameMismatchIssues(rows []kandangHouseTypeImportRow, kandangNameByID map[uint]string) []validationIssue {
	issues := make([]validationIssue, 0)
	for _, row := range rows {
		dbName, exists := kandangNameByID[row.KandangID]
		if !exists {
			continue
		}
		if strings.EqualFold(strings.TrimSpace(row.KandangName), strings.TrimSpace(dbName)) {
			continue
		}

		issues = append(issues, validationIssue{
			Row:     row.RowNumber,
			Field:   "kandang_name",
			Message: fmt.Sprintf("value %q does not match kandang_id %d name %q", row.KandangName, row.KandangID, dbName),
		})
	}
	return issues
}

func printPlanRows(rows []kandangHouseTypeImportRow, kandangNameByID map[uint]string) {
	for _, row := range rows {
		fmt.Printf(
			"PLAN row=%d kandang_id=%d kandang_name_file=%q kandang_name_db=%q house_type=%q\n",
			row.RowNumber,
			row.KandangID,
			row.KandangName,
			kandangNameByID[row.KandangID],
			row.HouseType,
		)
	}
}

func sortValidationIssues(issues []validationIssue) {
	sort.Slice(issues, func(i, j int) bool {
		if issues[i].Row == issues[j].Row {
			if issues[i].Field == issues[j].Field {
				return issues[i].Message < issues[j].Message
			}
			return issues[i].Field < issues[j].Field
		}
		return issues[i].Row < issues[j].Row
	})
}

func applyImportRows(
	ctx context.Context,
	runner txRunner,
	rows []kandangHouseTypeImportRow,
) ([]applyRowResult, int64, error) {
	results := make([]applyRowResult, 0, len(rows))
	normalizedNullCount := int64(0)

	err := runner.InTx(ctx, func(store kandangHouseTypeStore) error {
		for _, row := range rows {
			changed, err := store.UpdateKandangHouseType(ctx, row.KandangID, row.HouseType)
			if err != nil {
				return fmt.Errorf("row %d kandang_id=%d update failed: %w", row.RowNumber, row.KandangID, err)
			}

			results = append(results, applyRowResult{
				RowNumber: row.RowNumber,
				KandangID: row.KandangID,
				HouseType: row.HouseType,
				Changed:   changed,
			})
		}

		normalized, err := store.NormalizeNullHouseType(ctx)
		if err != nil {
			return fmt.Errorf("normalize null house_type to open_house failed: %w", err)
		}
		normalizedNullCount = normalized

		return nil
	})
	if err != nil {
		return nil, 0, err
	}

	return results, normalizedNullCount, nil
}

func (r dbTxRunner) InTx(ctx context.Context, fn func(store kandangHouseTypeStore) error) error {
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		return fn(dbKandangHouseTypeStore{db: tx})
	})
}

func (s dbKandangHouseTypeStore) UpdateKandangHouseType(ctx context.Context, kandangID uint, houseType string) (bool, error) {
	result := s.db.WithContext(ctx).Exec(`
		UPDATE kandangs
		SET house_type = ?::house_type_enum,
		    updated_at = NOW()
		WHERE id = ?
		  AND deleted_at IS NULL
		  AND house_type IS DISTINCT FROM ?::house_type_enum
	`, houseType, kandangID, houseType)
	if result.Error != nil {
		return false, result.Error
	}
	return result.RowsAffected > 0, nil
}

func (s dbKandangHouseTypeStore) NormalizeNullHouseType(ctx context.Context) (int64, error) {
	result := s.db.WithContext(ctx).Exec(`
		UPDATE kandangs
		SET house_type = 'open_house'::house_type_enum,
		    updated_at = NOW()
		WHERE deleted_at IS NULL
		  AND house_type IS NULL
	`)
	if result.Error != nil {
		return 0, result.Error
	}
	return result.RowsAffected, nil
}

func modeLabel(apply bool) string {
	if apply {
		return "APPLY"
	}
	return "DRY-RUN"
}

func applyStatus(changed bool) string {
	if changed {
		return "UPDATED"
	}
	return "UNCHANGED"
}

func countChangedRows(results []applyRowResult) int {
	count := 0
	for _, item := range results {
		if item.Changed {
			count++
		}
	}
	return count
}
