package executor

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"math/bits"
	"strconv"
	"strings"
	"time"
)

type cqlDuration struct {
	months int32
	days   int32
	nanos  int64
}

func encodeDurationValue(value interface{}) ([]byte, bool) {
	dur, ok := durationFromValue(value)
	if !ok {
		return nil, false
	}
	return encVints(dur.months, dur.days, dur.nanos), true
}

func decodeDurationBytes(data []byte) (cqlDuration, error) {
	if len(data) == 0 {
		return cqlDuration{}, nil
	}
	return decVints(data)
}

func formatDurationValue(d cqlDuration) string {
	if d.months == 0 && d.days == 0 && d.nanos == 0 {
		return "0"
	}
	neg := d.months < 0 || d.days < 0 || d.nanos < 0
	if (d.months < 0 && (d.days > 0 || d.nanos > 0)) ||
		(d.months > 0 && (d.days < 0 || d.nanos < 0)) ||
		(d.days < 0 && d.nanos > 0) ||
		(d.days > 0 && d.nanos < 0) {
		return fmt.Sprintf("%d:%d:%d", d.months, d.days, d.nanos)
	}

	sign := ""
	if neg {
		sign = "-"
	}
	months := absInt32(d.months)
	days := absInt32(d.days)
	nanos := absInt64(d.nanos)

	var b strings.Builder
	if sign != "" {
		b.WriteString(sign)
	}
	if months != 0 {
		b.WriteString(strconv.FormatInt(int64(months), 10))
		b.WriteString("mo")
	}
	if days != 0 {
		b.WriteString(strconv.FormatInt(int64(days), 10))
		b.WriteString("d")
	}
	if nanos != 0 {
		hours := nanos / int64(time.Hour)
		nanos %= int64(time.Hour)
		if hours != 0 {
			b.WriteString(strconv.FormatInt(hours, 10))
			b.WriteString("h")
		}
		minutes := nanos / int64(time.Minute)
		nanos %= int64(time.Minute)
		if minutes != 0 {
			b.WriteString(strconv.FormatInt(minutes, 10))
			b.WriteString("m")
		}
		seconds := nanos / int64(time.Second)
		nanos %= int64(time.Second)
		if seconds != 0 {
			b.WriteString(strconv.FormatInt(seconds, 10))
			b.WriteString("s")
		}
		millis := nanos / int64(time.Millisecond)
		nanos %= int64(time.Millisecond)
		if millis != 0 {
			b.WriteString(strconv.FormatInt(millis, 10))
			b.WriteString("ms")
		}
		micros := nanos / int64(time.Microsecond)
		nanos %= int64(time.Microsecond)
		if micros != 0 {
			b.WriteString(strconv.FormatInt(micros, 10))
			b.WriteString("us")
		}
		if nanos != 0 {
			b.WriteString(strconv.FormatInt(nanos, 10))
			b.WriteString("ns")
		}
	}
	if b.Len() == 0 {
		return "0"
	}
	return b.String()
}

func durationFromValue(value interface{}) (cqlDuration, bool) {
	switch v := value.(type) {
	case cqlDuration:
		return v, true
	case *cqlDuration:
		if v == nil {
			return cqlDuration{}, false
		}
		return *v, true
	case time.Duration:
		return cqlDuration{nanos: v.Nanoseconds()}, true
	case int:
		return cqlDuration{nanos: int64(v)}, true
	case int64:
		return cqlDuration{nanos: v}, true
	case json.Number:
		if i, err := v.Int64(); err == nil {
			return cqlDuration{nanos: i}, true
		}
		if f, err := v.Float64(); err == nil {
			return cqlDuration{nanos: int64(f)}, true
		}
	case string:
		if parsed, ok := parseDurationString(v); ok {
			return parsed, true
		}
	}
	return cqlDuration{}, false
}

func parseDurationString(value string) (cqlDuration, bool) {
	s := strings.TrimSpace(value)
	if s == "" {
		return cqlDuration{}, false
	}
	if strings.Contains(s, ":") {
		parts := strings.Split(s, ":")
		if len(parts) != 3 {
			return cqlDuration{}, false
		}
		months, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 32)
		if err != nil {
			return cqlDuration{}, false
		}
		days, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 32)
		if err != nil {
			return cqlDuration{}, false
		}
		nanos, err := strconv.ParseInt(strings.TrimSpace(parts[2]), 10, 64)
		if err != nil {
			return cqlDuration{}, false
		}
		return cqlDuration{months: int32(months), days: int32(days), nanos: nanos}, true
	}
	if s == "0" || s == "+0" || s == "-0" {
		return cqlDuration{}, true
	}
	sign := int64(1)
	if s[0] == '-' {
		sign = -1
		s = s[1:]
	} else if s[0] == '+' {
		s = s[1:]
	}
	if s == "" {
		return cqlDuration{}, false
	}
	s = strings.ToLower(s)

	var months int64
	var days int64
	var nanos int64
	for len(s) > 0 {
		i := 0
		for i < len(s) && s[i] >= '0' && s[i] <= '9' {
			i++
		}
		if i == 0 {
			return cqlDuration{}, false
		}
		numStr := s[:i]
		s = s[i:]
		if s == "" {
			num, err := strconv.ParseInt(numStr, 10, 64)
			if err != nil {
				return cqlDuration{}, false
			}
			nanos += num
			break
		}
		unit := ""
		switch {
		case strings.HasPrefix(s, "mo"):
			unit = "mo"
			s = s[2:]
		case strings.HasPrefix(s, "ms"):
			unit = "ms"
			s = s[2:]
		case strings.HasPrefix(s, "us"):
			unit = "us"
			s = s[2:]
		case strings.HasPrefix(s, "ns"):
			unit = "ns"
			s = s[2:]
		default:
			unit = s[:1]
			s = s[1:]
		}

		num, err := strconv.ParseInt(numStr, 10, 64)
		if err != nil {
			return cqlDuration{}, false
		}
		switch unit {
		case "y":
			months += num * 12
		case "mo":
			months += num
		case "w":
			days += num * 7
		case "d":
			days += num
		case "h":
			nanos += num * int64(time.Hour)
		case "m":
			nanos += num * int64(time.Minute)
		case "s":
			nanos += num * int64(time.Second)
		case "ms":
			nanos += num * int64(time.Millisecond)
		case "us":
			nanos += num * int64(time.Microsecond)
		case "ns":
			nanos += num
		default:
			return cqlDuration{}, false
		}
	}

	months *= sign
	days *= sign
	nanos *= sign
	if months < math.MinInt32 || months > math.MaxInt32 {
		return cqlDuration{}, false
	}
	if days < math.MinInt32 || days > math.MaxInt32 {
		return cqlDuration{}, false
	}
	return cqlDuration{months: int32(months), days: int32(days), nanos: nanos}, true
}

func absInt32(value int32) int32 {
	if value < 0 {
		return -value
	}
	return value
}

func absInt64(value int64) int64 {
	if value < 0 {
		return -value
	}
	return value
}

func decVints(data []byte) (cqlDuration, error) {
	months, i, err := decVint(data, 0)
	if err != nil {
		return cqlDuration{}, fmt.Errorf("failed to extract months: %w", err)
	}
	days, i, err := decVint(data, i)
	if err != nil {
		return cqlDuration{}, fmt.Errorf("failed to extract days: %w", err)
	}
	nanos, _, err := decVint(data, i)
	if err != nil {
		return cqlDuration{}, fmt.Errorf("failed to extract nanoseconds: %w", err)
	}
	return cqlDuration{months: int32(months), days: int32(days), nanos: nanos}, nil
}

func encVints(months int32, days int32, nanos int64) []byte {
	buf := append(encVint(int64(months)), encVint(int64(days))...)
	return append(buf, encVint(nanos)...)
}

func decVint(data []byte, start int) (int64, int, error) {
	if len(data) <= start {
		return 0, 0, errors.New("unexpected eof")
	}
	firstByte := data[start]
	if firstByte&0x80 == 0 {
		return decIntZigZag(uint64(firstByte)), start + 1, nil
	}
	numBytes := bits.LeadingZeros32(uint32(^firstByte)) - 24
	ret := uint64(firstByte & (0xff >> uint(numBytes)))
	if len(data) < start+numBytes+1 {
		return 0, 0, fmt.Errorf("expected %d bytes, have %d", start+numBytes+1, len(data))
	}
	for i := start; i < start+numBytes; i++ {
		ret <<= 8
		ret |= uint64(data[i+1] & 0xff)
	}
	return decIntZigZag(ret), start + numBytes + 1, nil
}

func encVint(v int64) []byte {
	vEnc := encIntZigZag(v)
	lead0 := bits.LeadingZeros64(vEnc)
	numBytes := (639 - lead0*9) >> 6
	if numBytes <= 1 {
		return []byte{byte(vEnc)}
	}
	extraBytes := numBytes - 1
	buf := make([]byte, numBytes)
	for i := extraBytes; i >= 0; i-- {
		buf[i] = byte(vEnc)
		vEnc >>= 8
	}
	buf[0] |= byte(^(0xff >> uint(extraBytes)))
	return buf
}

func decIntZigZag(n uint64) int64 {
	return int64((n >> 1) ^ -(n & 1))
}

func encIntZigZag(n int64) uint64 {
	return uint64((n >> 63) ^ (n << 1))
}
