package utils

import (
	"encoding/json"
	"fmt"
	"maps"
	"os"
	"sort"
	"strings"
)

// DataFile represents an AviaNZ .data file
type DataFile struct {
	Meta     *DataMeta
	Segments []*Segment
	FilePath string
}

// DataMeta contains metadata for a .data file
type DataMeta struct {
	Operator string
	Reviewer string
	Duration float64
	Extra    map[string]any // preserve unknown fields
}

// Segment represents a detection segment
type Segment struct {
	StartTime float64
	EndTime   float64
	FreqLow   float64
	FreqHigh  float64
	Labels    []*Label
}

// CallTypeNone is a sentinel value used in --species Species+_ to match
// only labels with an empty calltype.
const CallTypeNone = "_"

// Label represents a species label within a segment
type Label struct {
	Species   string
	Certainty int
	Filter    string
	CallType  string
	Comment   string         // user comment (max 140 chars, ASCII only)
	Bookmark  bool           // user bookmark for navigation
	Extra     map[string]any // preserve unknown fields
}

// ParseDataFile reads and parses a .data file
func ParseDataFile(path string) (*DataFile, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	var raw []json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, fmt.Errorf("parse JSON: %w", err)
	}

	if len(raw) == 0 {
		return nil, fmt.Errorf("empty .data file")
	}

	df := &DataFile{
		FilePath: path,
		Segments: make([]*Segment, 0, len(raw)-1),
	}

	// Parse metadata (first element)
	df.Meta = parseMeta(raw[0])

	// Parse segments
	for i := 1; i < len(raw); i++ {
		seg, err := parseSegment(raw[i])
		if err != nil {
			continue // skip invalid segments
		}
		df.Segments = append(df.Segments, seg)
	}

	// Sort segments by start time
	sort.Slice(df.Segments, func(i, j int) bool {
		return df.Segments[i].StartTime < df.Segments[j].StartTime
	})

	return df, nil
}

// parseMeta parses the metadata object
func parseMeta(raw json.RawMessage) *DataMeta {
	var obj map[string]any
	if err := json.Unmarshal(raw, &obj); err != nil {
		return &DataMeta{}
	}

	meta := &DataMeta{Extra: make(map[string]any)}

	if v, ok := obj["Operator"].(string); ok {
		meta.Operator = v
		delete(obj, "Operator")
	}
	if v, ok := obj["Reviewer"].(string); ok {
		meta.Reviewer = v
		delete(obj, "Reviewer")
	}
	if v, ok := obj["Duration"].(float64); ok {
		meta.Duration = v
		delete(obj, "Duration")
	}

	// Store remaining fields
	maps.Copy(meta.Extra, obj)

	return meta
}

// parseSegment parses a segment array
func parseSegment(raw json.RawMessage) (*Segment, error) {
	var arr []json.RawMessage
	if err := json.Unmarshal(raw, &arr); err != nil {
		return nil, err
	}

	if len(arr) < 5 {
		return nil, fmt.Errorf("segment too short")
	}

	seg := &Segment{}

	// Parse time and frequency
	if v, err := parseFloat(arr[0]); err == nil {
		seg.StartTime = v
	}
	if v, err := parseFloat(arr[1]); err == nil {
		seg.EndTime = v
	}
	if v, err := parseFloat(arr[2]); err == nil {
		seg.FreqLow = v
	}
	if v, err := parseFloat(arr[3]); err == nil {
		seg.FreqHigh = v
	}

	// Parse labels
	var labelArr []json.RawMessage
	if err := json.Unmarshal(arr[4], &labelArr); err == nil {
		for _, labelRaw := range labelArr {
			if label := parseLabel(labelRaw); label != nil {
				seg.Labels = append(seg.Labels, label)
			}
		}
	}

	// Sort labels alphabetically by species
	sort.Slice(seg.Labels, func(i, j int) bool {
		return seg.Labels[i].Species < seg.Labels[j].Species
	})

	return seg, nil
}

// parseLabel parses a label object
func parseLabel(raw json.RawMessage) *Label {
	var obj map[string]any
	if err := json.Unmarshal(raw, &obj); err != nil {
		return nil
	}

	label := &Label{Extra: make(map[string]any)}

	if v, ok := obj["species"].(string); ok {
		label.Species = v
		delete(obj, "species")
	}
	if v, ok := obj["certainty"].(float64); ok {
		label.Certainty = int(v)
		delete(obj, "certainty")
	}
	if v, ok := obj["filter"].(string); ok {
		label.Filter = v
		delete(obj, "filter")
	}
	if v, ok := obj["calltype"].(string); ok {
		label.CallType = v
		delete(obj, "calltype")
	}
	if v, ok := obj["comment"].(string); ok {
		label.Comment = v
		delete(obj, "comment")
	}
	if v, ok := obj["bookmark"].(bool); ok {
		label.Bookmark = v
		delete(obj, "bookmark")
	}

	// Store remaining fields
	maps.Copy(label.Extra, obj)

	return label
}

// parseFloat extracts a float from JSON
func parseFloat(raw json.RawMessage) (float64, error) {
	var v float64
	err := json.Unmarshal(raw, &v)
	return v, err
}

// WriteDataFile writes a DataFile back to disk
func (df *DataFile) Write(path string) error {
	var raw []any

	// Build metadata
	meta := make(map[string]any)
	if df.Meta.Operator != "" {
		meta["Operator"] = df.Meta.Operator
	}
	if df.Meta.Reviewer != "" {
		meta["Reviewer"] = df.Meta.Reviewer
	}
	if df.Meta.Duration > 0 {
		meta["Duration"] = df.Meta.Duration
	}
	maps.Copy(meta, df.Meta.Extra)
	raw = append(raw, meta)

	// Build segments
	for _, seg := range df.Segments {
		labels := make([]any, 0, len(seg.Labels))
		for _, label := range seg.Labels {
			l := make(map[string]any)
			l["species"] = label.Species
			l["certainty"] = label.Certainty
			if label.Filter != "" {
				l["filter"] = label.Filter
			}
			if label.CallType != "" {
				l["calltype"] = label.CallType
			}
			if label.Comment != "" {
				l["comment"] = label.Comment
			}
			if label.Bookmark {
				l["bookmark"] = true
			}
			maps.Copy(l, label.Extra)
			labels = append(labels, l)
		}

		segArr := []any{
			seg.StartTime,
			seg.EndTime,
			seg.FreqLow,
			seg.FreqHigh,
			labels,
		}
		raw = append(raw, segArr)
	}

	data, err := json.MarshalIndent(raw, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(path, data, 0644)
}

// HasFilterLabel returns true if segment has a label matching the filter
func (s *Segment) HasFilterLabel(filter string) bool {
	if filter == "" {
		return true
	}
	for _, label := range s.Labels {
		if label.Filter == filter {
			return true
		}
	}
	return false
}

// GetFilterLabels returns labels matching the filter
func (s *Segment) GetFilterLabels(filter string) []*Label {
	var result []*Label
	for _, label := range s.Labels {
		if filter == "" || label.Filter == filter {
			result = append(result, label)
		}
	}
	return result
}

// SegmentMatchesFilters returns true if the segment has any label matching all filter criteria.
// All non-empty/non-negative parameters must match for a label to be considered a match.
// Use certainty=-1 to indicate no certainty filtering (since 0 is a valid certainty value).
func (s *Segment) SegmentMatchesFilters(filter, species, callType string, certainty int) bool {
	if filter == "" && species == "" && callType == "" && certainty < 0 {
		return true // No filters, match all
	}

	for _, label := range s.Labels {
		if labelMatchesFilters(label, filter, species, callType, certainty) {
			return true
		}
	}
	return false
}

// labelMatchesFilters checks if a single label matches all filter criteria.
func labelMatchesFilters(label *Label, filter, species, callType string, certainty int) bool {
	if filter != "" && label.Filter != filter {
		return false
	}
	if species != "" && label.Species != species {
		return false
	}
	if callType == CallTypeNone {
		if label.CallType != "" {
			return false
		}
	} else if callType != "" && label.CallType != callType {
		return false
	}
	if certainty >= 0 && label.Certainty != certainty {
		return false
	}
	return true
}

// ParseSpeciesCallType parses a species string with optional calltype into separate values.
// Format: "Species" or "Species+CallType" (e.g., "Kiwi" or "Kiwi+Duet").
// Use "_" as the calltype to match only labels with no calltype (e.g., "Kiwi+_").
func ParseSpeciesCallType(label string) (species, callType string) {
	if label == "" {
		return "", ""
	}
	if before, after, ok := strings.Cut(label, "+"); ok {
		return before, after
	}
	return label, ""
}

// FindDataFiles finds all .data files in a folder, ignoring hidden files (starting with ".")
func FindDataFiles(folder string) ([]string, error) {
	return FindFiles(folder, FindFilesOptions{
		Extension:  ".data",
		Recursive:  false,
		SkipHidden: true,
	})
}