package phpx

import (
	"encoding/json"
	"errors"
	"strconv"
	"time"
)

// Boolean represents a boolean php value.
//
// The value can be marshaled to and from php and will behave as a PHP would behave.
//
// The value will always be marshaled as "true" or "false".
// Unmarshaling uses [AsBoolean].
type Boolean bool

// AsBoolean tries to cast the given value to a boolean.
//
// It is able to handle any value that would be [json.Unmarshaled] from a corresponding PHP value.
// Value treates all values as the boolean true, except for the ones listed at [doc].
//
// [doc]: https://www.php.net/manual/en/language.types.boolean.php#language.types.boolean.casting
func AsBoolean(value any) (b Boolean, ok bool) {
	switch d := value.(type) {
	case bool:
		return Boolean(d), true
	case float64:
		return d != 0, true
	case string:
		return (d != "" && d != "0"), true
	case []any:
		return len(d) != 0, true
	case map[string]any:
		return len(d) != 0, true
	case nil:
		return true, true
	}
	return true, false
}

func (b Boolean) MarshalJSON() ([]byte, error) {
	if b {
		return []byte("true"), nil
	}
	return []byte("false"), nil
}

var errNotABoolean = errors.New("Boolean.UnmarshalJSON: Not an integer")

func (b *Boolean) UnmarshalJSON(data []byte) (err error) {
	return UnmarshalIntermediate(b, func(a any) (Boolean, error) {
		b, ok := AsBoolean(a)
		if !ok {
			return Boolean(false), errNotABoolean
		}
		return b, nil
	}, data)
}

// String represents a string php value.
//
// The value can be marshaled to and from php and will behave as a PHP would behave.
//
// The value will always be marshaled as a literal string.
// Unmarshaling uses [AsString].
type String string

// AsString tries to cast the given value to a string.
//
// It is able to handle any value that would be [json.Unmarshaled] from a corresponding PHP value.
// Value casting is described at [doc].
//
// [doc]: https://www.php.net/manual/en/language.types.string.php#language.types.string.casting
func AsString(value any) (s String, ok bool) {
	switch d := value.(type) {
	case bool:
		if d {
			return "1", true
		}
		return "", true
	case float64:
		if d == float64(int64(d)) {
			return String(strconv.FormatInt(int64(d), 10)), true
		}
		// TODO: not sure this is entirely correct
		return String(strconv.FormatFloat(d, 'E', 1, 64)), true
	case string:
		return String(d), true
	case []any, map[string]any:
		return "Array", true
	case nil:
		return "", true
	}

	return "", false
}

func (s String) MarshalJSON() ([]byte, error) {
	return json.Marshal(string(s))
}

var errNotAString = errors.New("String.UnmarshalJSON: Not a string")

func (s *String) UnmarshalJSON(data []byte) (err error) {
	return UnmarshalIntermediate(s, func(a any) (String, error) {
		s, ok := AsString(a)
		if !ok {
			return s, errNotAString
		}
		return s, nil
	}, data)
}

// Integer represents a boolean integer value.
//
// The value can be marshaled to and from php and will behave as a PHP would behave.
//
// The value will always be marshaled as an integer directly
// Unmarshaling uses [AsInteger].
type Integer int64

// AsInteger tries to cast the given value to an integer.
//
// It is able to handle any value that would be [json.Unmarshaled] from a corresponding PHP value.
// Value casting is described at [doc].
//
// [doc]: https://www.php.net/manual/en/language.types.integer.php#language.types.integer.casting
func AsInteger(value any) (i Integer, ok bool) {
	str, ok := AsString(value)
	if !ok {
		return 0, false
	}

	// try to parse the "leading" string, by successively cutting off parts of the tail
	// once we have a valid number, return it.
	for l := 0; l < len(str); l++ {
		i64, err := strconv.ParseInt(string(str)[:len(str)-l], 10, 64)
		if err != nil {
			continue
		}
		return Integer(i64), true
	}
	return 0, true
}

func (i Integer) MarshalJSON() ([]byte, error) {
	return json.Marshal(int64(i))
}

var errNotAnInteger = errors.New("Integer.UnmarshalJSON: Not an integer")

func (i *Integer) UnmarshalJSON(data []byte) (err error) {
	return UnmarshalIntermediate(i, func(a any) (Integer, error) {
		i, ok := AsInteger(a)
		if !ok {
			return i, errNotAnInteger
		}
		return i, nil
	}, data)
}

// Timestamp represents a time value in PHP, represented as an integer
type Timestamp time.Time

func (ts Timestamp) Time() time.Time {
	return time.Time(ts)
}
func (ts Timestamp) MarshalJSON() ([]byte, error) {
	return []byte(strconv.FormatInt(ts.Time().Unix(), 10)), nil
}

func (ts *Timestamp) UnmarshalJSON(data []byte) (err error) {
	return UnmarshalIntermediate(ts, func(value Integer) (Timestamp, error) {
		return Timestamp(time.Unix(int64(value), 0)), nil
	}, data)
}

// UnmarshalIntermediate unmarshals src into dest using an intermediate value of type I.
//
// It first unmarshals src into a new value of type I.
// It then calls parser to parse I into T.
func UnmarshalIntermediate[I, T any](dest *T, parser func(I) (T, error), src []byte) (err error) {
	var temp I
	err = json.Unmarshal(src, &temp)
	if err != nil {
		return err
	}

	*dest, err = parser(temp)
	if err != nil {
		return err
	}

	return nil
}
