package excel

import (
	"app/internal/dialogs"
	"errors"
	"fmt"
	"github.com/kuzgoga/fogg"
	"github.com/xuri/excelize/v2"
	"log/slog"
	"reflect"
	"slices"
	"strings"
	"time"
)

type TableHeaders struct {
	Headers              []string
	IgnoredFieldsIndexes []int
}

func isPrimitiveType(valueType reflect.Type) bool {
	switch valueType.Kind() {
	case reflect.Bool,
		reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
		reflect.Float32, reflect.Float64,
		reflect.String:
		return true
	default:
		return false
	}
}

func DeleteDefaultSheet(file *excelize.File) error {
	sheetId, err := file.GetSheetIndex("Sheet1")
	if err != nil {
		return err
	}
	if sheetId != -1 {
		if err := file.DeleteSheet("Sheet1"); err != nil {
			return err
		}
	}
	return nil
}

func ExportEntitiesToSpreadsheet(filename string, exporters ...ExporterInterface) error {
	file := excelize.NewFile()
	defer func() {
		if err := file.Close(); err != nil {
			slog.Error("Error while closing excel file: ", err)
		}
	}()

	for _, exporter := range exporters {
		err := ExportEntityToSpreadsheet(file, exporter.GetSheetName(), exporter.GetEntity(), exporter.GetProvider())
		if err != nil {
			return err
		}
	}

	if err := DeleteDefaultSheet(file); err != nil {
		return err
	}

	err := WriteData(file, filename)
	if err != nil {
		return err
	}
	return nil
}

func ExportEntityToSpreadsheet[T any](file *excelize.File, sheetName string, entity T, provider func() ([]any, error)) error {
	_, err := file.NewSheet(sheetName)
	if err != nil {
		return err
	}

	headers, err := WriteHeaders(sheetName, entity, file)
	if err != nil {
		return err
	}

	items, err := provider()
	if err != nil {
		return err
	}

	dataStyleId, err := LoadDataStyle(file)
	if err != nil {
		return err
	}

	// TODO: appearance
	for i, item := range items {
		structValue := reflect.ValueOf(item).Elem()

		columnOffset := 0
		for j := 0; j < structValue.NumField(); j++ {
			if slices.Contains(headers.IgnoredFieldsIndexes, j) {
				columnOffset--
				continue
			}

			field := structValue.Field(j)
			tagLiteral := string(structValue.Type().Field(j).Tag)
			tag, err := fogg.Parse(tagLiteral)

			if err != nil {
				return err
			}

			if isPrimitiveType(field.Type()) {
				fieldValue := field.Interface()
				cellName, err := GetCellNameByIndices(j+columnOffset, i+1)
				if err != nil {
					return err
				}

				datatype := tag.GetTag("ui").GetParamOr("datatype", "")

				if datatype == timestampTag {
					err = file.SetCellValue(sheetName, cellName, time.Unix(fieldValue.(int64), 0))
				} else {
					err = file.SetCellValue(sheetName, cellName, fieldValue)
				}

				slog.Info(fmt.Sprintf("Field %s value: %v, %s\n", cellName, fieldValue, datatype))

				if err != nil {
					return err
				}
			} else {
				cellName, err := GetCellNameByIndices(j+columnOffset, i+1)
				if err != nil {
					return err
				}

				structInfo := structValue.Type().Field(j)
				structVal := structValue.Field(j)

				value, err := SerializeNestedField(structInfo, structVal)
				if err != nil {
					return err
				}

				err = file.SetCellValue(sheetName, cellName, *value)
				if err != nil {
					return err
				}

				err = file.SetCellStyle(sheetName, cellName, cellName, dataStyleId)
				if err != nil {
					return err
				}
			}
		}
	}

	return nil
}

func getNestedFieldValue(obj reflect.Value, path string) (any, error) {
	fields := strings.Split(path, ".")
	val := obj

	for _, field := range fields {
		if field == "" {
			continue
		}

		if val.Kind() == reflect.Ptr {
			val = val.Elem()
		}

		val = val.FieldByName(field)

		if !val.IsValid() {
			return nil, fmt.Errorf("field %s not found", field)
		}
	}
	return val.Interface(), nil
}

func SerializeNestedField(fieldInfo reflect.StructField, fieldValue reflect.Value) (*string, error) {
	tag, err := fogg.Parse(string(fieldInfo.Tag))
	if err != nil {
		return nil, err
	}

	var uiTag fogg.Tag

	if tag.HasTag("ui") && tag.GetTag("ui").HasParam("field") {
		uiTag = *tag.GetTag("ui")
	} else {
		slog.Error("Fail to extract Field tag")
		return nil, nil
	}

	if uiTag.HasParam("field") {
		var result string
		fieldPath := uiTag.GetParam("field").Value
		if fieldInfo.Type.Kind() == reflect.Slice {
			var items = make([]string, fieldValue.Len())
			for i := 0; i < fieldValue.Len(); i++ {
				item := fieldValue.Index(i)
				nestedFieldValue, err := getNestedFieldValue(item, fieldPath)
				if err != nil {
					return nil, err
				}
				items[i] = fmt.Sprintf("%v", nestedFieldValue)
			}
			result = strings.Join(items, ", ")
		} else {
			nestedFieldValue, err := getNestedFieldValue(fieldValue, fieldPath)
			if err != nil {
				return nil, err
			}
			result = fmt.Sprintf("%v", nestedFieldValue)
		}
		return &result, nil
	} else {
		return nil, nil
	}
}

func GetHeaderCellNameByIndex(column int) (string, error) {
	colName, err := excelize.ColumnNumberToName(column + 1)
	if err != nil {
		return "", err
	}
	cellName, err := excelize.JoinCellName(colName, 1)
	if err != nil {
		return "", err
	}
	return cellName, nil
}

func GetCellNameByIndices(column int, row int) (string, error) {
	colName, err := excelize.ColumnNumberToName(column + 1)
	if err != nil {
		return "", err
	}
	cellName, err := excelize.JoinCellName(colName, row+1)
	if err != nil {
		return "", err
	}
	return cellName, nil
}

func ExportHeaders(entity any) (TableHeaders, error) {
	headers := TableHeaders{}
	v := reflect.TypeOf(entity)
	for i := range v.NumField() {
		tag, err := fogg.Parse(string(v.Field(i).Tag))
		if err != nil {
			return headers, errors.New(fmt.Sprintf("Error occured while tag parsing `%s`: %s", err, string(v.Field(i).Tag)))
		}

		uiTag := tag.GetTag("ui")
		if uiTag == nil {
			headers.IgnoredFieldsIndexes = append(headers.IgnoredFieldsIndexes, i)
			continue
		}

		label := uiTag.GetParamOr("label", uiTag.GetParamOr(excelNameTag, ""))

		if label != "" {
			headers.Headers = append(headers.Headers, label)
		} else {
			headers.IgnoredFieldsIndexes = append(headers.IgnoredFieldsIndexes, i)
		}
	}
	return headers, nil
}

func WriteHeaders(sheetName string, entity any, file *excelize.File) (TableHeaders, error) {
	headers, err := ExportHeaders(entity)
	if err != nil {
		return headers, err
	}

	for i, header := range headers.Headers {
		cellName, err := GetHeaderCellNameByIndex(i)
		if err != nil {
			return headers, err
		}

		err = file.SetCellValue(sheetName, cellName, header)
		if err != nil {
			return headers, err
		}
	}

	err = ApplyStyleHeaders(file, sheetName, headers)
	if err != nil {
		return headers, err
	}

	return headers, nil
}

func GetStyleId(f *excelize.File, style *excelize.Style) (int, error) {
	styleId, err := f.NewStyle(style)
	if err != nil {
		return 0, fmt.Errorf("error occured while creating a style: %w", err)
	}

	return styleId, nil
}

func ApplyStyleHeaders(file *excelize.File, sheetName string, headers TableHeaders) error {
	styleId, err := LoadHeadersStyle(file)
	if err != nil {
		return err
	}

	cellName, err := GetHeaderCellNameByIndex(len(headers.Headers) - 1)
	if err != nil {
		return err
	}

	err = file.SetCellStyle(sheetName, "A1", cellName, styleId)
	if err != nil {
		return err
	}

	return nil
}

func WriteData(file *excelize.File, filename string) error {
	filepath, err := dialogs.SaveFileDialog(filename)

	if !strings.HasSuffix(filepath, ".xlsx") {
		filepath += ".xlsx"
	}

	if err != nil {
		return err
	}

	if filepath == "" {
		dialogs.InfoDialog("Экспорт данных", "Операция отменена")
	}

	if err := file.SaveAs(filepath); err != nil {
		return err
	}
	return nil
}
