package query

import (
	"bytes"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"
)

// Time column formats known to dataset profiles. time_range is always optional;
// when set, values are converted into predicates on TimeColumn.
const (
	TimeFormatUnixSeconds   = "unix_seconds"   // integer unix seconds (logs.timestamp, topsql.timestamps)
	TimeFormatUnixFloat     = "unix_float"     // floating unix seconds (slowlogs.time)
	TimeFormatWindowOverlap = "window_overlap" // sqlstatement: "begin" < end AND "end" >= begin
	TimeFormatHourOfDay     = "hour_of_day"    // log-stats.hour (0-23); scope.date locks the day
)

// TimeRange is an optional half-open window [begin, end) for file-level time columns.
//
// Accepted JSON shapes (mix-and-match ok as long as both bounds resolve):
//
//	{"begin_time": 1718668800, "end_time": 1718672400}
//	{"begin": "2026-07-17T02:00:00Z", "end": "2026-07-17T03:00:00Z"}
//	{"begin_time": "2026-07-17 02:00:00", "end_time": "2026-07-17 03:00:00"}
type TimeRange struct {
	BeginTime *flexibleTime `json:"begin_time,omitempty"`
	EndTime   *flexibleTime `json:"end_time,omitempty"`
	Begin     *flexibleTime `json:"begin,omitempty"`
	End       *flexibleTime `json:"end,omitempty"`
}

type flexibleTime struct {
	unix int64
}

func (t *flexibleTime) UnmarshalJSON(data []byte) error {
	data = bytes.TrimSpace(data)
	if len(data) == 0 || bytes.Equal(data, []byte("null")) {
		return fmt.Errorf("time value must not be null")
	}
	if data[0] == '"' {
		var s string
		if err := json.Unmarshal(data, &s); err != nil {
			return err
		}
		u, err := parseTimeString(s)
		if err != nil {
			return err
		}
		t.unix = u
		return nil
	}
	var n json.Number
	dec := json.NewDecoder(bytes.NewReader(data))
	dec.UseNumber()
	if err := dec.Decode(&n); err != nil {
		return fmt.Errorf("time value must be unix seconds or datetime string: %w", err)
	}
	// Accept integer or float seconds; truncate toward zero for bounds.
	f, err := n.Float64()
	if err != nil {
		i, err2 := n.Int64()
		if err2 != nil {
			return fmt.Errorf("invalid unix time %q", n.String())
		}
		t.unix = i
		return nil
	}
	t.unix = int64(f)
	return nil
}

func (t flexibleTime) MarshalJSON() ([]byte, error) {
	return json.Marshal(t.unix)
}

func parseTimeString(s string) (int64, error) {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0, fmt.Errorf("empty time string")
	}
	if n, err := strconv.ParseInt(s, 10, 64); err == nil {
		return n, nil
	}
	if f, err := strconv.ParseFloat(s, 64); err == nil {
		return int64(f), nil
	}
	layouts := []string{
		time.RFC3339Nano,
		time.RFC3339,
		"2006-01-02T15:04:05",
		"2006-01-02 15:04:05",
		"2006-01-02 15:04",
		"2006-01-02",
	}
	for _, layout := range layouts {
		if ts, err := time.Parse(layout, s); err == nil {
			return ts.UTC().Unix(), nil
		}
		// Also try local-as-UTC for layouts without zone.
		if ts, err := time.ParseInLocation(layout, s, time.UTC); err == nil {
			return ts.Unix(), nil
		}
	}
	return 0, fmt.Errorf("invalid time %q (want unix seconds or RFC3339 / \"yyyy-MM-dd[ HH:mm[:ss]]\")", s)
}

func (tr *TimeRange) unixBounds() (begin, end int64, err error) {
	if tr == nil {
		return 0, 0, fmt.Errorf("time_range is nil")
	}
	b := firstFlexibleTime(tr.BeginTime, tr.Begin)
	e := firstFlexibleTime(tr.EndTime, tr.End)
	if b == nil || e == nil {
		return 0, 0, fmt.Errorf("time_range requires begin/begin_time and end/end_time")
	}
	if b.unix <= 0 || e.unix <= 0 || e.unix <= b.unix {
		return 0, 0, fmt.Errorf("invalid time_range: require begin < end (unix seconds)")
	}
	return b.unix, e.unix, nil
}

func firstFlexibleTime(vals ...*flexibleTime) *flexibleTime {
	for _, v := range vals {
		if v != nil {
			return v
		}
	}
	return nil
}

// TimeRangeUnix builds a time_range from unix-second bounds (tests / callers).
func TimeRangeUnix(begin, end int64) *TimeRange {
	return &TimeRange{
		BeginTime: &flexibleTime{unix: begin},
		EndTime:   &flexibleTime{unix: end},
	}
}

func buildTimeRangePredicates(tr *TimeRange, column, format string) ([]string, error) {
	if tr == nil {
		return nil, nil
	}
	if column == "" || format == "" {
		return nil, fmt.Errorf("time_range is not supported for this dataset (no time column); filter time in SQL instead")
	}
	begin, end, err := tr.unixBounds()
	if err != nil {
		return nil, err
	}
	switch format {
	case TimeFormatUnixSeconds:
		return []string{
			fmt.Sprintf("%s >= %d", quoteIdent(column), begin),
			fmt.Sprintf("%s < %d", quoteIdent(column), end),
		}, nil
	case TimeFormatUnixFloat:
		return []string{
			fmt.Sprintf("%s >= %d", quoteIdent(column), begin),
			fmt.Sprintf("%s < %d", quoteIdent(column), end),
		}, nil
	case TimeFormatWindowOverlap:
		// Overlapping windows: row.begin < range.end AND row.end >= range.begin
		beginCol, endCol := "begin", "end"
		if column != "" && column != "begin" {
			// Allow "begin,end" encoding in TimeColumn for clarity.
			parts := strings.Split(column, ",")
			if len(parts) == 2 {
				beginCol, endCol = strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
			}
		}
		return []string{
			fmt.Sprintf("%s < %d", quoteIdent(beginCol), end),
			fmt.Sprintf("%s >= %d", quoteIdent(endCol), begin),
		}, nil
	case TimeFormatHourOfDay:
		lo, hi, err := hourOfDayBounds(begin, end)
		if err != nil {
			return nil, err
		}
		var preds []string
		if lo > 0 {
			preds = append(preds, fmt.Sprintf("%s >= %d", quoteIdent(column), lo))
		}
		if hi < 24 {
			preds = append(preds, fmt.Sprintf("%s < %d", quoteIdent(column), hi))
		}
		// Full day [00:00, next midnight): no hour predicate needed.
		return preds, nil
	default:
		return nil, fmt.Errorf("unsupported time_format %q", format)
	}
}

// hourOfDayBounds maps [begin, end) onto UTC hour-of-day [lo, hi) within a single calendar day.
// scope.date is expected to lock that day; end may be exactly next-day 00:00:00.
func hourOfDayBounds(begin, end int64) (lo, hi int, err error) {
	b := time.Unix(begin, 0).UTC()
	e := time.Unix(end, 0).UTC()
	bDay := time.Date(b.Year(), b.Month(), b.Day(), 0, 0, 0, 0, time.UTC)
	nextMidnight := bDay.Add(24 * time.Hour)
	endIsNextMidnight := e.Equal(nextMidnight)
	eDay := time.Date(e.Year(), e.Month(), e.Day(), 0, 0, 0, 0, time.UTC)
	if !eDay.Equal(bDay) && !endIsNextMidnight {
		return 0, 0, fmt.Errorf("time_range for hour_of_day must stay within one UTC day (got %s .. %s); lock the day with scope.date",
			b.Format(time.RFC3339), e.Format(time.RFC3339))
	}
	lo = b.Hour()
	switch {
	case endIsNextMidnight:
		hi = 24
	case e.Minute() == 0 && e.Second() == 0 && e.Nanosecond() == 0:
		hi = e.Hour()
	default:
		hi = e.Hour() + 1
	}
	if hi > 24 {
		hi = 24
	}
	if lo < 0 || lo > 23 || hi <= lo {
		return 0, 0, fmt.Errorf("invalid time_range hour bounds [%d, %d)", lo, hi)
	}
	return lo, hi, nil
}
