package geojson

import (
	"fmt"
	"time"

	"b612.me/astro/internal/geodata"
	"b612.me/astro/moon"
)

const lunarOccultationEvent = "lunar-occultation"

// MarshalStarOccultation 将月掩恒星的全球掩带和中心线编码为 GeoJSON。
// MarshalStarOccultation encodes a global stellar occultation band and center line as GeoJSON.
func MarshalStarOccultation(path moon.StarOccultationPath) ([]byte, error) {
	return marshalStarOccultation(path, nil)
}

// MarshalStarOccultationWithTimeMarkers 编码恒星月掩，并沿中心线按固定间隔追加 Point 要素。
// MarshalStarOccultationWithTimeMarkers encodes a stellar occultation and adds Point Features at regular intervals along its center line.
func MarshalStarOccultationWithTimeMarkers(
	path moon.StarOccultationPath,
	options TimeMarkerOptions,
) ([]byte, error) {
	return marshalStarOccultation(path, &options)
}

func marshalStarOccultation(path moon.StarOccultationPath, markerOptions *TimeMarkerOptions) ([]byte, error) {
	if markerOptions != nil {
		if err := validateTimeMarkerOptions(*markerOptions); err != nil {
			return nil, err
		}
	}
	if err := validateStarOccultationPathData(path); err != nil {
		return nil, err
	}
	properties := map[string]interface{}{
		"target_type":       "star",
		"target_id":         path.TargetID,
		"complete":          path.Complete,
		"step_seconds":      path.Step.Seconds(),
		"target_spacing_km": path.TargetSpacingKM,
	}
	band, err := occultationBandPolygon(path.NorthernLimit, path.SouthernLimit)
	if err != nil {
		return nil, fmt.Errorf("geojson: stellar occultation band: %w", err)
	}
	value, err := multiPolygonGeometry([][]geodata.GeoPoint{band})
	if err != nil {
		return nil, fmt.Errorf("geojson: stellar occultation band: %w", err)
	}
	features := []feature{
		newFeature(lunarOccultationEvent, "occultation-band", value, cloneProperties(properties)),
	}
	if len(path.CenterLine) > 0 {
		features, err = appendOccultationPathLine(features, "center-line", path.CenterLine, properties)
		if err != nil {
			return nil, err
		}
	}
	features, err = appendOccultationPathLine(features, "north-limit", path.NorthernLimit, properties)
	if err != nil {
		return nil, err
	}
	features, err = appendOccultationPathLine(features, "south-limit", path.SouthernLimit, properties)
	if err != nil {
		return nil, err
	}
	if markerOptions != nil && len(path.CenterLine) > 0 {
		features, err = appendTimeMarkerFeatures(
			features,
			lunarOccultationEvent,
			"center-line",
			occultationPathSamples(path.CenterLine),
			*markerOptions,
		)
		if err != nil {
			return nil, err
		}
	}
	for _, marker := range []struct {
		role  string
		point moon.OccultationPathPoint
	}{
		{role: "start", point: path.Start},
		{role: "greatest", point: path.Greatest},
		{role: "end", point: path.End},
	} {
		features, err = appendOccultationPoint(features, marker.role, marker.point, properties)
		if err != nil {
			return nil, err
		}
	}
	return marshalFeatureCollection(features)
}

// MarshalPlanetOccultation 将月掩行星的部分掩、全掩和中心线编码为 GeoJSON。
// MarshalPlanetOccultation encodes partial, total, and center-line planetary occultation geometry as GeoJSON.
func MarshalPlanetOccultation(path moon.PlanetOccultationPath) ([]byte, error) {
	return marshalPlanetOccultation(path, nil)
}

// MarshalPlanetOccultationWithTimeMarkers 编码行星月掩，并沿中心线按固定间隔追加 Point 要素。
// MarshalPlanetOccultationWithTimeMarkers encodes a planetary occultation and adds Point Features at regular intervals along its center line.
func MarshalPlanetOccultationWithTimeMarkers(
	path moon.PlanetOccultationPath,
	options TimeMarkerOptions,
) ([]byte, error) {
	return marshalPlanetOccultation(path, &options)
}

func marshalPlanetOccultation(path moon.PlanetOccultationPath, markerOptions *TimeMarkerOptions) ([]byte, error) {
	if markerOptions != nil {
		if err := validateTimeMarkerOptions(*markerOptions); err != nil {
			return nil, err
		}
	}
	if err := path.Planet.Validate(); err != nil {
		return nil, fmt.Errorf("geojson: planetary occultation target: %w", err)
	}
	if err := validatePlanetOccultationPathData(path); err != nil {
		return nil, err
	}
	properties := map[string]interface{}{
		"target_type":             "planet",
		"target_id":               path.TargetID,
		"planet":                  string(path.Planet),
		"complete":                path.Complete,
		"has_total_band":          path.HasTotalBand,
		"total_complete":          path.TotalComplete,
		"step_seconds":            path.Step.Seconds(),
		"target_spacing_km":       path.TargetSpacingKM,
		"greatest_total_width_km": path.GreatestTotalWidthKM,
	}
	features := make([]feature, 0, len(path.PartialFootprints)+len(path.TotalFootprints)+12)
	var err error
	if len(path.PartialFootprints) > 0 {
		features, err = appendOccultationFootprints(
			features, "partial-footprint", path.PartialFootprints, properties,
		)
	} else {
		features, err = appendOccultationBand(
			features, "partial-band", path.NorthernLimit, path.SouthernLimit, properties,
		)
	}
	if err != nil {
		return nil, err
	}

	if path.HasTotalBand {
		if len(path.TotalFootprints) > 0 {
			features, err = appendOccultationFootprints(
				features, "total-footprint", path.TotalFootprints, properties,
			)
		} else {
			features, err = appendOccultationBand(
				features, "total-band", path.NorthernTotalLimit, path.SouthernTotalLimit, properties,
			)
		}
		if err != nil {
			return nil, err
		}
	}

	if len(path.CenterLine) > 0 {
		features, err = appendOccultationPathLine(features, "center-line", path.CenterLine, properties)
		if err != nil {
			return nil, err
		}
	}
	features, err = appendOccultationPathLine(features, "north-limit", path.NorthernLimit, properties)
	if err != nil {
		return nil, err
	}
	features, err = appendOccultationPathLine(features, "south-limit", path.SouthernLimit, properties)
	if err != nil {
		return nil, err
	}
	if path.HasTotalBand {
		features, err = appendOccultationPathLine(
			features, "north-total-limit", path.NorthernTotalLimit, properties,
		)
		if err != nil {
			return nil, err
		}
		features, err = appendOccultationPathLine(
			features, "south-total-limit", path.SouthernTotalLimit, properties,
		)
		if err != nil {
			return nil, err
		}
	}
	if markerOptions != nil && len(path.CenterLine) > 0 {
		features, err = appendTimeMarkerFeatures(
			features,
			lunarOccultationEvent,
			"center-line",
			occultationPathSamples(path.CenterLine),
			*markerOptions,
		)
		if err != nil {
			return nil, err
		}
	}

	markers := []struct {
		role  string
		point moon.OccultationPathPoint
	}{
		{role: "start", point: path.Start},
	}
	if path.HasTotalBand {
		markers = append(markers, struct {
			role  string
			point moon.OccultationPathPoint
		}{role: "total-start", point: path.TotalStart})
	}
	markers = append(markers, struct {
		role  string
		point moon.OccultationPathPoint
	}{role: "greatest", point: path.Greatest})
	if path.HasTotalBand {
		markers = append(markers, struct {
			role  string
			point moon.OccultationPathPoint
		}{role: "total-end", point: path.TotalEnd})
	}
	markers = append(markers, struct {
		role  string
		point moon.OccultationPathPoint
	}{role: "end", point: path.End})
	for _, marker := range markers {
		features, err = appendOccultationPoint(features, marker.role, marker.point, properties)
		if err != nil {
			return nil, err
		}
	}
	return marshalFeatureCollection(features)
}

func appendOccultationBand(
	features []feature,
	role string,
	northern, southern []moon.OccultationPathPoint,
	properties map[string]interface{},
) ([]feature, error) {
	band, err := occultationBandPolygon(northern, southern)
	if err != nil {
		return nil, fmt.Errorf("geojson: %s: %w", role, err)
	}
	value, err := multiPolygonGeometry([][]geodata.GeoPoint{band})
	if err != nil {
		return nil, fmt.Errorf("geojson: %s: %w", role, err)
	}
	return append(features, newFeature(
		lunarOccultationEvent, role, value, cloneProperties(properties),
	)), nil
}

func appendOccultationFootprints(
	features []feature,
	role string,
	footprints []moon.PlanetOccultationFootprint,
	properties map[string]interface{},
) ([]feature, error) {
	appended := 0
	for _, footprint := range footprints {
		if footprint.Time.IsZero() {
			return nil, fmt.Errorf("geojson: %s time is required", role)
		}
		polygons := make([][]geodata.GeoPoint, 0, len(footprint.Polygons))
		for _, source := range footprint.Polygons {
			polygon := make([]geodata.GeoPoint, len(source))
			for index, point := range source {
				polygon[index] = geodata.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude}
			}
			polygons = append(polygons, polygon)
		}
		value, err := multiPolygonGeometry(polygons)
		if err != nil {
			return nil, fmt.Errorf("geojson: %s at %s: %w", role, formatTime(footprint.Time), err)
		}
		footprintProperties := cloneProperties(properties)
		footprintProperties["time"] = formatTime(footprint.Time)
		features = append(features, newFeature(
			lunarOccultationEvent, role, value, footprintProperties,
		))
		appended++
	}
	if appended == 0 {
		return nil, fmt.Errorf("geojson: %s has no valid polygons", role)
	}
	return features, nil
}

func occultationBandPolygon(
	northern, southern []moon.OccultationPathPoint,
) ([]geodata.GeoPoint, error) {
	if len(northern) != len(southern) {
		return nil, fmt.Errorf("paired limits must have the same sample count")
	}
	count := len(northern)
	if count < 2 {
		return nil, fmt.Errorf("paired limits require at least two points per side")
	}
	for index := range northern {
		if northern[index].Time.IsZero() || southern[index].Time.IsZero() {
			return nil, fmt.Errorf("paired limit sample %d time is required", index)
		}
		if !northern[index].Time.Equal(southern[index].Time) {
			return nil, fmt.Errorf("paired limit sample %d times must match", index)
		}
	}
	polygon := make([]geodata.GeoPoint, 0, 2*count)
	for _, point := range northern[:count] {
		polygon = append(polygon, geodata.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude})
	}
	for index := count - 1; index >= 0; index-- {
		point := southern[index]
		polygon = append(polygon, geodata.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude})
	}
	return polygon, nil
}

func appendOccultationPathLine(
	features []feature,
	role string,
	points []moon.OccultationPathPoint,
	properties map[string]interface{},
) ([]feature, error) {
	samples := make([]pathSample, len(points))
	for index, point := range points {
		samples[index] = occultationPathSample(point)
	}
	return appendTimedLineFeature(features, lunarOccultationEvent, role, samples, properties)
}

func occultationPathSamples(points []moon.OccultationPathPoint) []pathSample {
	samples := make([]pathSample, len(points))
	for index, point := range points {
		samples[index] = occultationPathSample(point)
	}
	return samples
}

func appendOccultationPoint(
	features []feature,
	role string,
	point moon.OccultationPathPoint,
	properties map[string]interface{},
) ([]feature, error) {
	pointProperties := cloneProperties(properties)
	pointProperties["moon_altitude_deg"] = point.MoonAltitude
	pointProperties["width_km"] = point.WidthKM
	return appendPointFeature(
		features, lunarOccultationEvent, role, occultationPathSample(point), pointProperties,
	)
}

func occultationPathSample(point moon.OccultationPathPoint) pathSample {
	return pathSample{Time: point.Time, Longitude: point.Longitude, Latitude: point.Latitude}
}

func validateStarOccultationPathData(path moon.StarOccultationPath) error {
	if !path.Complete {
		return fmt.Errorf("geojson: occultation path is incomplete")
	}
	if err := (moon.OccultationPathOptions{Step: path.Step, TargetSpacingKM: path.TargetSpacingKM}).Validate(); err != nil {
		return fmt.Errorf("geojson: invalid occultation path sampling metadata: %w", err)
	}
	if err := validateOccultationPathPoint("start", path.Start); err != nil {
		return err
	}
	if err := validateOccultationPathPoint("greatest", path.Greatest); err != nil {
		return err
	}
	if err := validateOccultationPathPoint("end", path.End); err != nil {
		return err
	}
	if path.Greatest.Time.Before(path.Start.Time) || path.End.Time.Before(path.Greatest.Time) {
		return fmt.Errorf("geojson: occultation times must be ordered start, greatest, end")
	}
	if err := validateOccultationPathSeries("center line", path.CenterLine, false); err != nil {
		return err
	}
	if err := validateOccultationPathSeries("northern limit", path.NorthernLimit, true); err != nil {
		return err
	}
	if err := validateOccultationPathSeries("southern limit", path.SouthernLimit, true); err != nil {
		return err
	}
	if len(path.NorthernLimit) != len(path.SouthernLimit) {
		return fmt.Errorf("geojson: occultation northern and southern limits must have the same sample count")
	}
	for index := range path.NorthernLimit {
		if !path.NorthernLimit[index].Time.Equal(path.SouthernLimit[index].Time) {
			return fmt.Errorf("geojson: occultation limit sample %d times must match", index)
		}
	}
	last := len(path.NorthernLimit) - 1
	if !path.NorthernLimit[0].Time.Equal(path.Start.Time) || !path.SouthernLimit[0].Time.Equal(path.Start.Time) ||
		!path.NorthernLimit[last].Time.Equal(path.End.Time) || !path.SouthernLimit[last].Time.Equal(path.End.Time) {
		return fmt.Errorf("geojson: occultation limits must span start through end")
	}
	if len(path.CenterLine) > 0 {
		if path.CenterLine[0].Time.Before(path.Start.Time) ||
			path.CenterLine[len(path.CenterLine)-1].Time.After(path.End.Time) {
			return fmt.Errorf("geojson: occultation center line must be inside start and end")
		}
		if path.Greatest.Time.Before(path.CenterLine[0].Time) ||
			path.Greatest.Time.After(path.CenterLine[len(path.CenterLine)-1].Time) {
			return fmt.Errorf("geojson: occultation greatest time is outside the center-line interval")
		}
	}
	return nil
}

func validatePlanetOccultationPathData(path moon.PlanetOccultationPath) error {
	starPath := moon.StarOccultationPath{
		TargetID: path.TargetID, Start: path.Start, Greatest: path.Greatest, End: path.End,
		Complete: path.Complete, CenterLine: path.CenterLine,
		NorthernLimit: path.NorthernLimit, SouthernLimit: path.SouthernLimit,
		Step: path.Step, TargetSpacingKM: path.TargetSpacingKM,
	}
	if err := validateStarOccultationPathData(starPath); err != nil {
		return err
	}
	if !path.HasTotalBand {
		if path.TotalComplete || !path.TotalStart.Time.IsZero() || !path.TotalEnd.Time.IsZero() ||
			len(path.NorthernTotalLimit) != 0 || len(path.SouthernTotalLimit) != 0 ||
			len(path.TotalFootprints) != 0 || path.GreatestTotalWidthKM != 0 {
			return fmt.Errorf("geojson: total-band fields require HasTotalBand")
		}
		return validateOccultationFootprints("partial", path.PartialFootprints, path.Start.Time, path.End.Time)
	}
	if !path.TotalComplete {
		return fmt.Errorf("geojson: total-occultation band is incomplete")
	}
	if err := validateOccultationPathPoint("total start", path.TotalStart); err != nil {
		return err
	}
	if err := validateOccultationPathPoint("total end", path.TotalEnd); err != nil {
		return err
	}
	if !path.Start.Time.Before(path.TotalStart.Time) || !path.TotalStart.Time.Before(path.Greatest.Time) ||
		!path.Greatest.Time.Before(path.TotalEnd.Time) || !path.TotalEnd.Time.Before(path.End.Time) {
		return fmt.Errorf("geojson: total-band times must be inside outer start, greatest, and end")
	}
	if !finiteGeoJSON(path.GreatestTotalWidthKM) || path.GreatestTotalWidthKM <= 0 ||
		!finiteGeoJSON(path.Greatest.WidthKM) || path.GreatestTotalWidthKM >= path.Greatest.WidthKM {
		return fmt.Errorf("geojson: total-band width must be positive and narrower than the outer band")
	}
	if err := validateOccultationPathSeries("northern total limit", path.NorthernTotalLimit, true); err != nil {
		return err
	}
	if err := validateOccultationPathSeries("southern total limit", path.SouthernTotalLimit, true); err != nil {
		return err
	}
	if len(path.NorthernTotalLimit) != len(path.SouthernTotalLimit) {
		return fmt.Errorf("geojson: total northern and southern limits must have the same sample count")
	}
	for index := range path.NorthernTotalLimit {
		if !path.NorthernTotalLimit[index].Time.Equal(path.SouthernTotalLimit[index].Time) {
			return fmt.Errorf("geojson: total limit sample %d times must match", index)
		}
	}
	last := len(path.NorthernTotalLimit) - 1
	if !path.NorthernTotalLimit[0].Time.Equal(path.TotalStart.Time) ||
		!path.SouthernTotalLimit[0].Time.Equal(path.TotalStart.Time) ||
		!path.NorthernTotalLimit[last].Time.Equal(path.TotalEnd.Time) ||
		!path.SouthernTotalLimit[last].Time.Equal(path.TotalEnd.Time) {
		return fmt.Errorf("geojson: total limits must span total start through total end")
	}
	if err := validateOccultationFootprints("partial", path.PartialFootprints, path.Start.Time, path.End.Time); err != nil {
		return err
	}
	return validateOccultationFootprints("total", path.TotalFootprints, path.TotalStart.Time, path.TotalEnd.Time)
}

func validateOccultationPathSeries(name string, points []moon.OccultationPathPoint, required bool) error {
	if required && len(points) < 2 {
		return fmt.Errorf("geojson: %s requires at least two points", name)
	}
	previous := time.Time{}
	for index, point := range points {
		if err := validateOccultationPathPoint(fmt.Sprintf("%s[%d]", name, index), point); err != nil {
			return err
		}
		if !previous.IsZero() && !point.Time.After(previous) {
			return fmt.Errorf("geojson: %s times must be strictly increasing", name)
		}
		previous = point.Time
	}
	return nil
}

func validateOccultationPathPoint(name string, point moon.OccultationPathPoint) error {
	if point.Time.IsZero() {
		return fmt.Errorf("geojson: %s time is required", name)
	}
	if err := validateCoordinate(point.Longitude, point.Latitude); err != nil {
		return fmt.Errorf("geojson: %s: %w", name, err)
	}
	if !finiteGeoJSON(point.MoonAltitude) || point.MoonAltitude < -90 || point.MoonAltitude > 90 {
		return fmt.Errorf("geojson: %s Moon altitude must be finite and within [-90, 90]", name)
	}
	if !finiteGeoJSON(point.WidthKM) || point.WidthKM < 0 {
		return fmt.Errorf("geojson: %s width must be finite and non-negative", name)
	}
	return nil
}

func validateOccultationFootprints(
	name string,
	footprints []moon.PlanetOccultationFootprint,
	start, end time.Time,
) error {
	previous := time.Time{}
	for index, footprint := range footprints {
		if footprint.Time.IsZero() {
			return fmt.Errorf("geojson: %s footprint[%d] time is required", name, index)
		}
		if footprint.Time.Before(start) || footprint.Time.After(end) {
			return fmt.Errorf("geojson: %s footprint[%d] time must be inside its contact interval", name, index)
		}
		if !previous.IsZero() && !footprint.Time.After(previous) {
			return fmt.Errorf("geojson: %s footprint times must be strictly increasing", name)
		}
		previous = footprint.Time
	}
	return nil
}
