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"
	"gorm.io/gorm"
)

type importOptions struct {
	FilePath string
	Sheet    string
	Apply    bool
}

type headerIndexes struct {
	AdjustmentID int
	Weight       int
}

type adjustmentPriceImportRow struct {
	RowNumber    int
	AdjustmentID uint
	Weight       float64
}

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 adjustmentResolver interface {
	ResolveExistingAdjustmentIDs(ctx context.Context, adjustmentIDs []uint) (map[uint]struct{}, error)
}

type dbAdjustmentResolver struct {
	db *gorm.DB
}

type adjustmentPriceStore interface {
	UpdatePrice(ctx context.Context, adjustmentID uint, price float64) (bool, error)
}

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

type dbTxRunner struct {
	db *gorm.DB
}

type dbAdjustmentPriceStore struct {
	db *gorm.DB
}

type applyRowResult struct {
	RowNumber    int
	AdjustmentID uint
	Price        float64
	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 := parseAdjustmentPriceFile(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 := dbAdjustmentResolver{db: db}

	existingAdjustmentIDs, err := resolver.ResolveExistingAdjustmentIDs(ctx, collectAdjustmentIDs(rows))
	if err != nil {
		log.Fatalf("failed checking adjustment_id against adjustment_stocks: %v", err)
	}

	processableRows, skippedRows := splitRowsByExistingIDs(rows, existingAdjustmentIDs)
	issues := append([]validationIssue{}, parseIssues...)
	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.Printf("Rows processable: %d\n", len(processableRows))
	fmt.Printf("Rows skipped_missing: %d\n", len(skippedRows))
	fmt.Println()

	if len(processableRows) > 0 {
		printPlanRows(processableRows)
	}
	if len(skippedRows) > 0 {
		printSkippedRows(skippedRows)
	}
	if len(processableRows) > 0 || len(skippedRows) > 0 {
		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 processable=%d skipped_missing=%d applied=0 failed=%d\n",
			len(rows),
			len(processableRows),
			len(skippedRows),
			len(issues),
		)
		os.Exit(1)
	}

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

	results, err := applyIfRequested(ctx, true, dbTxRunner{db: db}, processableRows)
	if err != nil {
		log.Fatalf("apply failed: %v", err)
	}

	for _, result := range results {
		fmt.Printf(
			"DONE row=%d adjustment_id=%d price=%.3f status=%s\n",
			result.RowNumber,
			result.AdjustmentID,
			result.Price,
			applyStatus(result.Changed),
		)
	}

	appliedCount := countChangedRows(results)
	if len(results) > 0 {
		fmt.Println()
	}
	fmt.Printf(
		"Summary: planned=%d processable=%d skipped_missing=%d applied=%d failed=0\n",
		len(rows),
		len(processableRows),
		len(skippedRows),
		appliedCount,
	)
}

func parseAdjustmentPriceFile(
	filePath string,
	requestedSheet string,
) (string, []adjustmentPriceImportRow, []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
	}

	rowsByAdjustmentID := make(map[uint]adjustmentPriceImportRow)
	issues := make([]validationIssue, 0)

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

		if isRowEmpty(rawRow) {
			continue
		}

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

		rowsByAdjustmentID[parsed.AdjustmentID] = *parsed
	}

	rows := make([]adjustmentPriceImportRow, 0, len(rowsByAdjustmentID))
	for _, row := range rowsByAdjustmentID {
		rows = append(rows, row)
	}
	sort.Slice(rows, func(i, j int) bool {
		return rows[i].RowNumber < rows[j].RowNumber
	})

	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{AdjustmentID: -1, Weight: -1}
	issues := make([]validationIssue, 0)

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

		switch header {
		case "adjustment_id":
			if indexes.AdjustmentID >= 0 {
				issues = append(issues, validationIssue{Field: "header", Message: "duplicate header adjustment_id"})
			}
			indexes.AdjustmentID = idx
		case "weight":
			if indexes.Weight >= 0 {
				issues = append(issues, validationIssue{Field: "header", Message: "duplicate header weight"})
			}
			indexes.Weight = idx
		}
	}

	if indexes.AdjustmentID < 0 {
		issues = append(issues, validationIssue{Field: "adjustment_id", Message: "required header is missing"})
	}
	if indexes.Weight < 0 {
		issues = append(issues, validationIssue{Field: "weight", Message: "required header is missing"})
	}

	return indexes, issues
}

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

	adjustmentIDRaw := strings.TrimSpace(cellValue(rawRow, indexes.AdjustmentID))
	adjustmentID, err := parsePositiveUint(adjustmentIDRaw)
	if err != nil {
		issues = append(issues, validationIssue{Row: rowNumber, Field: "adjustment_id", Message: err.Error()})
	}

	weightRaw := strings.TrimSpace(cellValue(rawRow, indexes.Weight))
	weight, err := parseNonNegativeFloat(weightRaw)
	if err != nil {
		issues = append(issues, validationIssue{Row: rowNumber, Field: "weight", Message: err.Error()})
	}

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

	return &adjustmentPriceImportRow{
		RowNumber:    rowNumber,
		AdjustmentID: adjustmentID,
		Weight:       weight,
	}, nil
}

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 parseNonNegativeFloat(raw string) (float64, error) {
	if raw == "" {
		return 0, fmt.Errorf("is required")
	}

	value, err := strconv.ParseFloat(raw, 64)
	if err != nil {
		return 0, fmt.Errorf("must be numeric")
	}
	if value < 0 {
		return 0, fmt.Errorf("must be greater than or equal to 0")
	}

	return value, 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 collectAdjustmentIDs(rows []adjustmentPriceImportRow) []uint {
	ids := make([]uint, 0, len(rows))
	seen := make(map[uint]struct{}, len(rows))
	for _, row := range rows {
		if row.AdjustmentID == 0 {
			continue
		}
		if _, exists := seen[row.AdjustmentID]; exists {
			continue
		}
		seen[row.AdjustmentID] = struct{}{}
		ids = append(ids, row.AdjustmentID)
	}
	sort.Slice(ids, func(i, j int) bool {
		return ids[i] < ids[j]
	})
	return ids
}

func (r dbAdjustmentResolver) ResolveExistingAdjustmentIDs(
	ctx context.Context,
	adjustmentIDs []uint,
) (map[uint]struct{}, error) {
	result := make(map[uint]struct{})
	if len(adjustmentIDs) == 0 {
		return result, nil
	}

	type adjustmentIDRow struct {
		ID uint `gorm:"column:id"`
	}

	rows := make([]adjustmentIDRow, 0, len(adjustmentIDs))
	if err := r.db.WithContext(ctx).
		Table("adjustment_stocks").
		Select("id").
		Where("id IN ?", adjustmentIDs).
		Scan(&rows).Error; err != nil {
		return nil, err
	}

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

	return result, nil
}

func splitRowsByExistingIDs(
	rows []adjustmentPriceImportRow,
	existing map[uint]struct{},
) ([]adjustmentPriceImportRow, []adjustmentPriceImportRow) {
	processable := make([]adjustmentPriceImportRow, 0, len(rows))
	skipped := make([]adjustmentPriceImportRow, 0)

	for _, row := range rows {
		if _, exists := existing[row.AdjustmentID]; exists {
			processable = append(processable, row)
			continue
		}
		skipped = append(skipped, row)
	}

	return processable, skipped
}

func printPlanRows(rows []adjustmentPriceImportRow) {
	for _, row := range rows {
		fmt.Printf(
			"PLAN row=%d adjustment_id=%d price=%.3f\n",
			row.RowNumber,
			row.AdjustmentID,
			row.Weight,
		)
	}
}

func printSkippedRows(rows []adjustmentPriceImportRow) {
	for _, row := range rows {
		fmt.Printf(
			"SKIP row=%d adjustment_id=%d reason=adjustment_id not found\n",
			row.RowNumber,
			row.AdjustmentID,
		)
	}
}

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 applyIfRequested(
	ctx context.Context,
	apply bool,
	runner txRunner,
	rows []adjustmentPriceImportRow,
) ([]applyRowResult, error) {
	if !apply || len(rows) == 0 {
		return nil, nil
	}
	return applyImportRows(ctx, runner, rows)
}

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

	err := runner.InTx(ctx, func(store adjustmentPriceStore) error {
		for _, row := range rows {
			changed, err := store.UpdatePrice(ctx, row.AdjustmentID, row.Weight)
			if err != nil {
				return fmt.Errorf("row %d adjustment_id=%d update failed: %w", row.RowNumber, row.AdjustmentID, err)
			}

			results = append(results, applyRowResult{
				RowNumber:    row.RowNumber,
				AdjustmentID: row.AdjustmentID,
				Price:        row.Weight,
				Changed:      changed,
			})
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	return results, nil
}

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

func (s dbAdjustmentPriceStore) UpdatePrice(
	ctx context.Context,
	adjustmentID uint,
	price float64,
) (bool, error) {
	result := s.db.WithContext(ctx).Exec(`
		UPDATE adjustment_stocks
		SET price = ?,
		    updated_at = NOW()
		WHERE id = ?
		  AND price IS DISTINCT FROM ?
	`, price, adjustmentID, price)
	if result.Error != nil {
		return false, result.Error
	}
	return result.RowsAffected > 0, 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 _, result := range results {
		if result.Changed {
			count++
		}
	}
	return count
}
