package healthcheck

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

// CheckItem represents a single check item within a strategy
type CheckItem struct {
	Name        string `json:"name" binding:"required"`      // Item name (unique within strategy)
	Description string `json:"description"`                  // Item description
	CheckType   string `json:"checkType" binding:"required"` // sql, metrics, log, etc.
	Params      string `json:"params"`                       // JSON formatted parameters
	Enabled     bool   `json:"enabled"`                      // Whether this item is enabled
	Failed      bool   `json:"failed,omitempty"`             // Whether this item failed during execution
	ErrorMsg    string `json:"errorMsg,omitempty"`           // Error message if failed
}

// UnmarshalJSON accepts both legacy string params and object params.
func (i *CheckItem) UnmarshalJSON(data []byte) error {
	type checkItemAlias CheckItem
	var raw struct {
		*checkItemAlias
		Params json.RawMessage `json:"params"`
	}
	raw.checkItemAlias = (*checkItemAlias)(i)
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	i.Params = ""
	if len(raw.Params) == 0 || bytes.Equal(bytes.TrimSpace(raw.Params), []byte("null")) {
		return nil
	}

	var paramsString string
	if err := json.Unmarshal(raw.Params, &paramsString); err == nil {
		i.Params = paramsString
		return nil
	}

	var compact bytes.Buffer
	if err := json.Compact(&compact, raw.Params); err != nil {
		return err
	}
	i.Params = compact.String()
	return nil
}

// HealthCheckStrategyReq represents a request to create/update health check strategy
type HealthCheckStrategyReq struct {
	Name        string `json:"name" binding:"required"`
	Description string `json:"description"`

	// Schedule configuration
	ScheduleType string `json:"scheduleType"` // manual, cron
	ScheduleExpr string `json:"scheduleExpr"` // cron expression

	// Default time range for all check items (string format)
	// Supported formats:
	// - "now": current time
	// - numeric string (e.g., "-3600"): relative time offset in seconds from now
	// - time string (e.g., "2026-01-01T00:00:00Z"): absolute time
	// - "HH:MM" (range A only, both ends): a time-of-day inspection window. For
	//   cron strategies the backend resolves it to the most recent occurrence
	//   before the trigger, interpreting the clock in the CRON_TZ timezone.
	TimeRangeAStart string `json:"timeRangeAStart"`
	TimeRangeAEnd   string `json:"timeRangeAEnd"`
	TimeRangeBStart string `json:"timeRangeBStart"`
	TimeRangeBEnd   string `json:"timeRangeBEnd"`

	// Check items as structured array
	Items []CheckItem `json:"items"`
}

// parseTimeString parses a time string according to the following rules:
// 1. "now" -> current time
// 2. numeric string -> relative time offset in seconds from now
// 3. time string -> absolute time
// 4. other -> error
func ParseTimeString(s string) (*time.Time, error) {
	if s == "" {
		return nil, nil
	}

	// Rule 1: "now" -> current time
	if s == "now" {
		now := time.Now()
		return &now, nil
	}

	// Rule 2: numeric string -> relative time offset in seconds from now
	if offset, err := strconv.ParseInt(s, 10, 64); err == nil {
		t := time.Now().Add(time.Duration(offset) * time.Second)
		return &t, nil
	}

	// Rule 3: time string -> absolute time
	// Try multiple time formats
	formats := []string{
		time.RFC3339,
		"2006-01-02T15:04:05",
		"2006-01-02 15:04:05",
		"2006-01-02",
	}

	for _, format := range formats {
		if t, err := time.Parse(format, s); err == nil {
			return &t, nil
		}
	}

	// Rule 4: other -> error
	return nil, fmt.Errorf("invalid time format: %s", s)
}

// ParseTimeOfDay parses a strict "HH:MM" 24-hour clock string (e.g. "10:00")
// used to express an inspection window's start/end.
// It returns the offset from midnight and ok=true only for a valid time-of-day.
// Anything else (relative seconds like "-86400", "now", RFC3339, "HH:MM:SS")
// returns ok=false so callers fall back to ParseTimeString. This keeps the
// new format from colliding with the existing expression formats.
func ParseTimeOfDay(s string) (time.Duration, bool) {
	if len(s) != 5 || s[2] != ':' {
		return 0, false
	}
	for _, idx := range []int{0, 1, 3, 4} {
		if s[idx] < '0' || s[idx] > '9' {
			return 0, false
		}
	}
	hh := int(s[0]-'0')*10 + int(s[1]-'0')
	mm := int(s[3]-'0')*10 + int(s[4]-'0')
	if hh > 23 || mm > 59 {
		return 0, false
	}
	return time.Duration(hh)*time.Hour + time.Duration(mm)*time.Minute, true
}

// ResolveTimeOfDayWindow resolves an inspection window expressed as a pair of
// time-of-day offsets into an absolute [start, end] range, by walking backwards
// from base to the most recent occurrence of that window.
//
//   - The end is the latest occurrence of endTOD that is not strictly after
//     base, so when endTOD equals the trigger clock the window stays on the
//     current day; when base falls inside a cross-midnight window the result is
//     the previous fully-completed window.
//   - The start is on the same calendar day for an intraday window (startTOD <
//     endTOD), or the previous calendar day when the window wraps past midnight
//     or spans a full day (startTOD == endTOD).
//   - loc is the timezone in which the clock times are interpreted (taken from
//     the cron CRON_TZ= prefix). It must match the cron trigger's timezone.
//   - base is the trigger time (cron scan "now" / manual trigger "now").
//
// Both ends are built with time.Date in loc so the wall-clock times stay correct
// across DST transitions (a full-day window is one calendar day — 23h or 25h on a
// switch day — which is the intended "same clock, yesterday" semantics). The
// returned times are in UTC.
func ResolveTimeOfDayWindow(startTOD, endTOD time.Duration, loc *time.Location, base time.Time) (time.Time, time.Time) {
	if loc == nil {
		loc = time.Local
	}
	startHour := int(startTOD / time.Hour)
	startMin := int(startTOD % time.Hour / time.Minute)
	endHour := int(endTOD / time.Hour)
	endMin := int(endTOD % time.Hour / time.Minute)

	b := base.In(loc)
	y, m, d := b.Date()
	// End wall-clock on base's calendar day, walked back a day if it is after base.
	absEnd := time.Date(y, m, d, endHour, endMin, 0, 0, loc)
	if absEnd.After(b) {
		absEnd = time.Date(y, m, d-1, endHour, endMin, 0, 0, loc)
	}
	// Start wall-clock on the same calendar day as the end for an intraday window,
	// or the previous day when the window wraps past midnight / spans a full day.
	ey, em, ed := absEnd.Date()
	if startTOD >= endTOD {
		ed--
	}
	absStart := time.Date(ey, em, ed, startHour, startMin, 0, 0, loc)
	return absStart.UTC(), absEnd.UTC()
}

// ResolveRangeA resolves the primary (A) time range into absolute times for a
// result. When both ends are an "HH:MM" inspection window it is resolved as a
// pair against base in the cron's timezone (ResolveTimeOfDayWindow); otherwise
// each end falls back to ParseTimeString. startStr/endStr are the already
// resolved expression strings (Expr field preferred, legacy fallback). base is
// the trigger time. Either return value may be nil when its input is empty or
// unparseable. This is the single integration point exercised by both the cron
// scanner and unit tests, so the "expr -> result time" path is testable without
// a database.
func ResolveRangeA(startStr, endStr, scheduleExpr string, base time.Time) (*time.Time, *time.Time) {
	if startTOD, ok1 := ParseTimeOfDay(startStr); ok1 {
		if endTOD, ok2 := ParseTimeOfDay(endStr); ok2 {
			loc := ExtractCronLocation(scheduleExpr)
			absStart, absEnd := ResolveTimeOfDayWindow(startTOD, endTOD, loc, base)
			return &absStart, &absEnd
		}
	}
	var start, end *time.Time
	if startStr != "" {
		if t, err := ParseTimeString(startStr); err == nil {
			start = t
		}
	}
	if endStr != "" {
		if t, err := ParseTimeString(endStr); err == nil {
			end = t
		}
	}
	return start, end
}

// ValidateTimeRange validates the time-range expressions of the request.
// The A range may use the "HH:MM" time-of-day form (inspection window), but in
// that case BOTH ends must be time-of-day. Every other (non time-of-day)
// expression must be parseable by ParseTimeString.
func (r *HealthCheckStrategyReq) ValidateTimeRange() error {
	_, aStartTOD := ParseTimeOfDay(r.TimeRangeAStart)
	_, aEndTOD := ParseTimeOfDay(r.TimeRangeAEnd)
	if aStartTOD != aEndTOD {
		return fmt.Errorf("inspection window requires both start and end in HH:MM format")
	}

	checks := []struct {
		name  string
		val   string
		isTOD bool
	}{
		{"timeRangeAStart", r.TimeRangeAStart, aStartTOD},
		{"timeRangeAEnd", r.TimeRangeAEnd, aEndTOD},
		{"timeRangeBStart", r.TimeRangeBStart, false},
		{"timeRangeBEnd", r.TimeRangeBEnd, false},
	}
	for _, c := range checks {
		if c.isTOD || c.val == "" {
			continue
		}
		if _, err := ParseTimeString(c.val); err != nil {
			return fmt.Errorf("invalid %s: %w", c.name, err)
		}
	}
	return nil
}

// HealthCheckStrategyResp represents a health check strategy response
type HealthCheckStrategyResp struct {
	UUID       string    `json:"uuid"`
	ClusterID  string    `json:"clusterId"`
	DbaasOrgID string    `json:"dbaasOrgId"` // dbaas org id from URL
	CreatorID  uint      `json:"creatorId"`
	Creator    string    `json:"creator"`
	CreatedAt  time.Time `json:"createdAt"`
	UpdatedAt  time.Time `json:"updatedAt"`

	Name        string `json:"name"`
	Description string `json:"description"`

	ScheduleType string `json:"scheduleType"`
	ScheduleExpr string `json:"scheduleExpr"`

	TimeRangeAStart string `json:"timeRangeAStart"`
	TimeRangeAEnd   string `json:"timeRangeAEnd"`
	TimeRangeBStart string `json:"timeRangeBStart"`
	TimeRangeBEnd   string `json:"timeRangeBEnd"`

	// Check items as structured array
	Items []CheckItem `json:"items,omitempty"`

	Status string `json:"status"`

	LastRunAt *time.Time `json:"lastRunAt"`
	NextRunAt *time.Time `json:"nextRunAt"`
}

// ListHealthCheckStrategyResp represents a list of health check strategies
type ListHealthCheckStrategyResp struct {
	Total      uint                      `json:"total"`
	Strategies []HealthCheckStrategyResp `json:"strategies"`
}

// TriggerHealthCheckReq represents a request to trigger a health check
type TriggerHealthCheckReq struct {
	// Optional: specific item names to run (if empty, run all enabled items)
	ItemNames []string `json:"itemNames,omitempty"`

	// Optional time range override (string format)
	// Supported formats:
	// - "now": current time
	// - numeric string (e.g., "-3600"): relative time offset in seconds from now
	// - time string (e.g., "2026-01-01T00:00:00Z"): absolute time
	TimeRangeAStart string `json:"timeRangeAStart"`
	TimeRangeAEnd   string `json:"timeRangeAEnd"`
	TimeRangeBStart string `json:"timeRangeBStart"`
	TimeRangeBEnd   string `json:"timeRangeBEnd"`
}

// HealthCheckResultResp represents a health check result response
type HealthCheckResultResp struct {
	UUID         string    `json:"uuid"`
	StrategyUUID string    `json:"strategyUuid"`
	ClusterID    string    `json:"clusterId"`
	DbaasOrgID   string    `json:"dbaasOrgId"` // dbaas org id from URL
	ItemsToRun   []string  `json:"itemsToRun"` // Items to run (from strategy)
	Creator      string    `json:"creator"`
	CreatedAt    time.Time `json:"createdAt"`
	UpdatedAt    time.Time `json:"updatedAt"`

	Status   string `json:"status"`
	ErrorMsg string `json:"errorMsg"`

	TimeRangeAStart *time.Time `json:"timeRangeAStart"`
	TimeRangeAEnd   *time.Time `json:"timeRangeAEnd"`
	TimeRangeBStart *time.Time `json:"timeRangeBStart"`
	TimeRangeBEnd   *time.Time `json:"timeRangeBEnd"`

	Metadata string `json:"metadata"`
}

// ListHealthCheckResultResp represents a list of health check results
type ListHealthCheckResultResp struct {
	Total   uint                    `json:"total"`
	Results []HealthCheckResultResp `json:"results"`
}

// HealthCheckTaskResp represents a health check task response
type HealthCheckTaskResp struct {
	ResultUUID string `json:"resultUuid"`
}

// HealthCheckClusterUrls represents cluster URLs for health check
type HealthCheckClusterUrls struct {
	ClusterMetricsUrl   string `json:"clusterMetricsUrl"`
	DataPlaneMetricsUrl string `json:"dataPlaneMetricsUrl"`
	SlowQueryS3Url      string `json:"slowQueryS3Url"`
}

// HealthCheckTimeRange represents a time range for health check
type HealthCheckTimeRange struct {
	Start string `json:"start"`
	End   string `json:"end"`
}

// HealthCheckJobRequest represents the request body for health check job
type HealthCheckJobRequest struct {
	Name               string                 `json:"name"`
	ClusterId          string                 `json:"clusterId"`
	PhysicalClusterId  string                 `json:"physicalClusterId"`
	ReportId           string                 `json:"reportId"`
	DeployType         string                 `json:"deployType"`
	ProjectId          string                 `json:"projectId"`
	Provider           string                 `json:"provider"`
	Region             string                 `json:"region"`
	ClinicOrgId        string                 `json:"clinicOrgId"`
	TenantId           string                 `json:"tenantId"`
	Version            string                 `json:"version"`
	DashboardVersion   string                 `json:"dashboardVersion"`
	TimeRange          HealthCheckTimeRange   `json:"timeRange"`
	ClusterUrls        HealthCheckClusterUrls `json:"clusterUrls"`
	ClusterMeta        map[string]string      `json:"clusterMeta,omitempty"`
	Params             json.RawMessage        `json:"params"`             // TODO: use
	DataSource         string                 `json:"dataSource"`         // TODO: remove
	MemMaxThreshold    uint64                 `json:"memMaxThreshold"`    // TODO: remove
	QueryTimeThreshold uint64                 `json:"queryTimeThreshold"` // TODO: remove
	Options            map[string]string      `json:"options"`
}

// HealthCheckCombinedReport represents a combined report with all check item results
type HealthCheckCombinedReport struct {
	StrategyUUID    string                  `json:"strategyUuid"`
	ClusterID       string                  `json:"clusterId"`
	DbaasOrgID      string                  `json:"dbaasOrgId"`
	CreatedAt       time.Time               `json:"createdAt"`
	Status          string                  `json:"status"` // success, partial, failed
	TimeRangeAStart *time.Time              `json:"timeRangeAStart"`
	TimeRangeAEnd   *time.Time              `json:"timeRangeAEnd"`
	TimeRangeBStart *time.Time              `json:"timeRangeBStart"`
	TimeRangeBEnd   *time.Time              `json:"timeRangeBEnd"`
	Items           []HealthCheckItemReport `json:"items"`
}

// HealthCheckItemReport represents a single check item's report
type HealthCheckItemReport struct {
	Name        string          `json:"name"`
	Description string          `json:"description,omitempty"`
	CheckType   string          `json:"checkType,omitempty"`
	Params      json.RawMessage `json:"params,omitempty" swaggertype:"object"`
	Status      string          `json:"status"` // success, failed
	ErrorMsg    string          `json:"errorMsg,omitempty"`
	Content     json.RawMessage `json:"content,omitempty" swaggertype:"object"` // The actual report content from S3
}
