package main

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

	"github.com/xuri/excelize/v2"
	"gitlab.com/mbugroup/lti-api.git/internal/config"
	"gitlab.com/mbugroup/lti-api.git/internal/database"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gorm.io/gorm"
)

const dateLayout = "2006-01-02"

type importOptions struct {
	FilePath string
	Sheet    string
	Apply    bool
}

type headerIndexes struct {
	ProjectFlockID int
	TotalCost      int
	CutoverDate    int
	Note           int
}

type manualInputImportRow struct {
	RowNumber      int
	ProjectFlockID uint
	TotalCost      float64
	CutoverDate    time.Time
	Note           *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 farmResolver interface {
	ResolveActiveLayingFarms(ctx context.Context, projectFlockIDs []uint) (map[uint]string, error)
}

type dbFarmResolver struct {
	db *gorm.DB
}

type manualInputStore interface {
	UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error
	DeleteSnapshotsFromDate(ctx context.Context, fromDate time.Time, farmIDs []uint) error
}

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

type dbTxRunner struct {
	db *gorm.DB
}

type expenseDepreciationStore struct {
	repo repportRepo.ExpenseDepreciationRepository
}

type farmIdentityRow struct {
	ID       uint   `gorm:"column:id"`
	FarmName string `gorm:"column:farm_name"`
}

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")
	}

	location, err := time.LoadLocation("Asia/Jakarta")
	if err != nil {
		log.Fatalf("failed to load timezone Asia/Jakarta: %v", err)
	}

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

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

	farmNameByID, err := resolver.ResolveActiveLayingFarms(ctx, collectProjectFlockIDs(rows))
	if err != nil {
		log.Fatalf("failed validating project_flock_id against project_flocks: %v", err)
	}

	issues := append([]validationIssue{}, parseIssues...)
	issues = append(issues, buildMissingFarmIssues(rows, farmNameByID)...)
	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, farmNameByID)
		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 failed=%d\n", len(rows), len(issues))
		os.Exit(1)
	}

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

	if len(rows) == 0 {
		fmt.Println("Summary: planned=0 applied=0 failed=0")
		return
	}

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

	for _, row := range rows {
		fmt.Printf(
			"DONE row=%d project_flock_id=%d cutover_date=%s\n",
			row.RowNumber,
			row.ProjectFlockID,
			row.CutoverDate.In(location).Format(dateLayout),
		)
	}

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

func parseManualInputFile(
	filePath string,
	requestedSheet string,
	location *time.Location,
) (string, []manualInputImportRow, []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([]manualInputImportRow, 0, len(allRows)-1)
	issues := make([]validationIssue, 0)
	seenProjectFlockIDs := 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, location, seenProjectFlockIDs)
		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{
		ProjectFlockID: -1,
		TotalCost:      -1,
		CutoverDate:    -1,
		Note:           -1,
	}
	issues := make([]validationIssue, 0)

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

		switch header {
		case "project_flock_id":
			if indexes.ProjectFlockID >= 0 {
				issues = append(issues, validationIssue{
					Field:   "header",
					Message: "duplicate header project_flock_id",
				})
			}
			indexes.ProjectFlockID = idx
		case "total_cost":
			if indexes.TotalCost >= 0 {
				issues = append(issues, validationIssue{
					Field:   "header",
					Message: "duplicate header total_cost",
				})
			}
			indexes.TotalCost = idx
		case "cutover_date":
			if indexes.CutoverDate >= 0 {
				issues = append(issues, validationIssue{
					Field:   "header",
					Message: "duplicate header cutover_date",
				})
			}
			indexes.CutoverDate = idx
		case "note":
			if indexes.Note >= 0 {
				issues = append(issues, validationIssue{
					Field:   "header",
					Message: "duplicate header note",
				})
			}
			indexes.Note = idx
		}
	}

	if indexes.ProjectFlockID < 0 {
		issues = append(issues, validationIssue{
			Field:   "project_flock_id",
			Message: "required header is missing",
		})
	}
	if indexes.TotalCost < 0 {
		issues = append(issues, validationIssue{
			Field:   "total_cost",
			Message: "required header is missing",
		})
	}
	if indexes.CutoverDate < 0 {
		issues = append(issues, validationIssue{
			Field:   "cutover_date",
			Message: "required header is missing",
		})
	}

	return indexes, issues
}

func parseDataRow(
	rawRow []string,
	rowNumber int,
	indexes headerIndexes,
	location *time.Location,
	seenProjectFlockIDs map[uint]int,
) (*manualInputImportRow, []validationIssue) {
	issues := make([]validationIssue, 0)

	projectFlockIDRaw := strings.TrimSpace(cellValue(rawRow, indexes.ProjectFlockID))
	projectFlockID, err := parsePositiveUint(projectFlockIDRaw)
	if err != nil {
		issues = append(issues, validationIssue{
			Row:     rowNumber,
			Field:   "project_flock_id",
			Message: err.Error(),
		})
	}

	totalCostRaw := strings.TrimSpace(cellValue(rawRow, indexes.TotalCost))
	totalCost, err := parseNonNegativeFloat(totalCostRaw)
	if err != nil {
		issues = append(issues, validationIssue{
			Row:     rowNumber,
			Field:   "total_cost",
			Message: err.Error(),
		})
	}

	cutoverDateRaw := strings.TrimSpace(cellValue(rawRow, indexes.CutoverDate))
	cutoverDate, err := parseDateOnlyInLocation(cutoverDateRaw, location)
	if err != nil {
		issues = append(issues, validationIssue{
			Row:     rowNumber,
			Field:   "cutover_date",
			Message: err.Error(),
		})
	}

	var note *string
	noteRaw := strings.TrimSpace(cellValue(rawRow, indexes.Note))
	if noteRaw != "" {
		if len([]rune(noteRaw)) > 1000 {
			issues = append(issues, validationIssue{
				Row:     rowNumber,
				Field:   "note",
				Message: "must have at most 1000 characters",
			})
		} else {
			note = &noteRaw
		}
	}

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

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

	return &manualInputImportRow{
		RowNumber:      rowNumber,
		ProjectFlockID: projectFlockID,
		TotalCost:      totalCost,
		CutoverDate:    cutoverDate,
		Note:           note,
	}, 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 parseDateOnlyInLocation(raw string, location *time.Location) (time.Time, error) {
	if raw == "" {
		return time.Time{}, fmt.Errorf("is required")
	}
	value, err := time.ParseInLocation(dateLayout, raw, location)
	if err != nil {
		return time.Time{}, fmt.Errorf("must follow format YYYY-MM-DD")
	}
	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 collectProjectFlockIDs(rows []manualInputImportRow) []uint {
	ids := make([]uint, 0, len(rows))
	seen := make(map[uint]struct{}, len(rows))
	for _, row := range rows {
		if row.ProjectFlockID == 0 {
			continue
		}
		if _, exists := seen[row.ProjectFlockID]; exists {
			continue
		}
		seen[row.ProjectFlockID] = struct{}{}
		ids = append(ids, row.ProjectFlockID)
	}
	sort.Slice(ids, func(i, j int) bool {
		return ids[i] < ids[j]
	})
	return ids
}

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

	rows := make([]farmIdentityRow, 0, len(projectFlockIDs))
	if err := r.db.WithContext(ctx).
		Table("project_flocks").
		Select("id, flock_name AS farm_name").
		Where("id IN ?", projectFlockIDs).
		Where("deleted_at IS NULL").
		Where("category = ?", utils.ProjectFlockCategoryLaying).
		Scan(&rows).Error; err != nil {
		return nil, err
	}

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

	return result, nil
}

func buildMissingFarmIssues(rows []manualInputImportRow, farmNameByID map[uint]string) []validationIssue {
	issues := make([]validationIssue, 0)
	for _, row := range rows {
		if _, exists := farmNameByID[row.ProjectFlockID]; exists {
			continue
		}
		issues = append(issues, validationIssue{
			Row:     row.RowNumber,
			Field:   "project_flock_id",
			Message: fmt.Sprintf("value %d must reference an active LAYING project_flock", row.ProjectFlockID),
		})
	}
	return issues
}

func printPlanRows(rows []manualInputImportRow, farmNameByID map[uint]string) {
	for _, row := range rows {
		farmName := farmNameByID[row.ProjectFlockID]
		fmt.Printf(
			"PLAN row=%d project_flock_id=%d farm_name=%q total_cost=%.3f cutover_date=%s note=%q\n",
			row.RowNumber,
			row.ProjectFlockID,
			farmName,
			row.TotalCost,
			row.CutoverDate.Format(dateLayout),
			derefString(row.Note),
		)
	}
}

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 []manualInputImportRow) error {
	if !apply || len(rows) == 0 {
		return nil
	}
	return applyImportRows(ctx, runner, rows)
}

func applyImportRows(ctx context.Context, runner txRunner, rows []manualInputImportRow) error {
	return runner.InTx(ctx, func(store manualInputStore) error {
		for _, row := range rows {
			payload := entity.FarmDepreciationManualInput{
				ProjectFlockId: row.ProjectFlockID,
				TotalCost:      row.TotalCost,
				CutoverDate:    row.CutoverDate,
				Note:           row.Note,
			}

			if err := store.UpsertManualInput(ctx, &payload); err != nil {
				return fmt.Errorf("row %d project_flock_id=%d upsert failed: %w", row.RowNumber, row.ProjectFlockID, err)
			}

			if err := store.DeleteSnapshotsFromDate(ctx, row.CutoverDate, []uint{row.ProjectFlockID}); err != nil {
				return fmt.Errorf("row %d project_flock_id=%d snapshot invalidation failed: %w", row.RowNumber, row.ProjectFlockID, err)
			}
		}
		return nil
	})
}

func (r dbTxRunner) InTx(ctx context.Context, fn func(store manualInputStore) error) error {
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		repo := repportRepo.NewExpenseDepreciationRepository(tx)
		store := expenseDepreciationStore{repo: repo}
		return fn(store)
	})
}

func (s expenseDepreciationStore) UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error {
	return s.repo.UpsertManualInput(ctx, row)
}

func (s expenseDepreciationStore) DeleteSnapshotsFromDate(ctx context.Context, fromDate time.Time, farmIDs []uint) error {
	return s.repo.DeleteSnapshotsFromDate(ctx, fromDate, farmIDs)
}

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

func derefString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}
