package criteria

import (
	"strconv"
	"time"
)

// Conjunctions need to implement this interface, to allow Criteria to extract child playlist IDs recursively
type conjunction interface {
	ChildPlaylistIds() []string
}

type (
	All []Expression
	And = All
)

func (All) fields() map[string]any { return nil }

func (all All) MarshalJSON() ([]byte, error) {
	return marshalConjunction("all", all)
}

func (all All) ChildPlaylistIds() (ids []string) {
	return extractPlaylistIds(all)
}

type (
	Any []Expression
	Or  = Any
)

func (Any) fields() map[string]any { return nil }

func (any Any) MarshalJSON() ([]byte, error) {
	return marshalConjunction("any", any)
}

func (any Any) ChildPlaylistIds() (ids []string) {
	return extractPlaylistIds(any)
}

type Is map[string]any
type Eq = Is

func (is Is) MarshalJSON() ([]byte, error) {
	return marshalExpression("is", is)
}

func (is Is) fields() map[string]any { return is }

type IsNot map[string]any

func (isn IsNot) MarshalJSON() ([]byte, error) {
	return marshalExpression("isNot", isn)
}

func (isn IsNot) fields() map[string]any { return isn }

type Gt map[string]any

func (gt Gt) MarshalJSON() ([]byte, error) {
	return marshalExpression("gt", gt)
}

func (gt Gt) fields() map[string]any { return gt }

type Lt map[string]any

func (lt Lt) MarshalJSON() ([]byte, error) {
	return marshalExpression("lt", lt)
}

func (lt Lt) fields() map[string]any { return lt }

type Before map[string]any

func (bf Before) MarshalJSON() ([]byte, error) {
	return marshalExpression("before", bf)
}

func (bf Before) fields() map[string]any { return bf }

type After Gt

func (af After) MarshalJSON() ([]byte, error) {
	return marshalExpression("after", af)
}

func (af After) fields() map[string]any { return af }

type Contains map[string]any

func (ct Contains) MarshalJSON() ([]byte, error) {
	return marshalExpression("contains", ct)
}

func (ct Contains) fields() map[string]any { return ct }

type NotContains map[string]any

func (nct NotContains) MarshalJSON() ([]byte, error) {
	return marshalExpression("notContains", nct)
}

func (nct NotContains) fields() map[string]any { return nct }

type StartsWith map[string]any

func (sw StartsWith) MarshalJSON() ([]byte, error) {
	return marshalExpression("startsWith", sw)
}

func (sw StartsWith) fields() map[string]any { return sw }

type EndsWith map[string]any

func (ew EndsWith) MarshalJSON() ([]byte, error) {
	return marshalExpression("endsWith", ew)
}

func (ew EndsWith) fields() map[string]any { return ew }

type InTheRange map[string]any

func (itr InTheRange) MarshalJSON() ([]byte, error) {
	return marshalExpression("inTheRange", itr)
}

func (itr InTheRange) fields() map[string]any { return itr }

type InTheLast map[string]any

func (itl InTheLast) MarshalJSON() ([]byte, error) {
	return marshalExpression("inTheLast", itl)
}

func (itl InTheLast) fields() map[string]any { return itl }

type NotInTheLast map[string]any

func (nitl NotInTheLast) MarshalJSON() ([]byte, error) {
	return marshalExpression("notInTheLast", nitl)
}

func (nitl NotInTheLast) fields() map[string]any { return nitl }

func startOfPeriod(numDays int64, from time.Time) string {
	return from.Add(time.Duration(-24*numDays) * time.Hour).Format("2006-01-02")
}

type InPlaylist map[string]any

func (ipl InPlaylist) MarshalJSON() ([]byte, error) {
	return marshalExpression("inPlaylist", ipl)
}

func (ipl InPlaylist) fields() map[string]any { return ipl }

type NotInPlaylist map[string]any

func (nipl NotInPlaylist) MarshalJSON() ([]byte, error) {
	return marshalExpression("notInPlaylist", nipl)
}

func (nipl NotInPlaylist) fields() map[string]any { return nipl }

type IsMissing map[string]any

func (im IsMissing) MarshalJSON() ([]byte, error) {
	return marshalExpression("isMissing", im)
}

func (im IsMissing) fields() map[string]any { return im }

type IsPresent map[string]any

func (ip IsPresent) MarshalJSON() ([]byte, error) {
	return marshalExpression("isPresent", ip)
}

func (ip IsPresent) fields() map[string]any { return ip }

func IsTruthy(v any) bool {
	switch val := v.(type) {
	case bool:
		return val
	case float64:
		return val != 0
	case string:
		b, err := strconv.ParseBool(val)
		return err == nil && b
	default:
		return v != nil
	}
}

func extractPlaylistIds(inputRule any) (ids []string) {
	var id string
	var ok bool

	switch rule := inputRule.(type) {
	case Any:
		for _, rules := range rule {
			ids = append(ids, extractPlaylistIds(rules)...)
		}
	case All:
		for _, rules := range rule {
			ids = append(ids, extractPlaylistIds(rules)...)
		}
	case InPlaylist:
		if id, ok = rule["id"].(string); ok {
			ids = append(ids, id)
		}
	case NotInPlaylist:
		if id, ok = rule["id"].(string); ok {
			ids = append(ids, id)
		}
	}

	return
}
