package tools

import (
	"fmt"
	"math"
	"os"
	"strings"

	"skraak/utils"
)

// CallsModifyInput defines the input for the modify tool
type CallsModifyInput struct {
	File      string `json:"file"`
	Reviewer  string `json:"reviewer"`
	Filter    string `json:"filter"`
	Segment   string `json:"segment"`
	Certainty int    `json:"certainty"`
	Species   string `json:"species"`
	Bookmark  *bool  `json:"bookmark"`
	Comment   string `json:"comment"`
}

// CallsModifyOutput defines the output for the modify tool
type CallsModifyOutput struct {
	File          string `json:"file"`
	SegmentStart  int    `json:"segment_start"`
	SegmentEnd    int    `json:"segment_end"`
	Species       string `json:"species,omitempty"`
	CallType      string `json:"calltype,omitempty"`
	Certainty     int    `json:"certainty,omitempty"`
	Bookmark      *bool  `json:"bookmark,omitempty"`
	Comment       string `json:"comment,omitempty"`
	PreviousValue string `json:"previous_value,omitempty"`
	Error         string `json:"error,omitempty"`
}

// validateModifyInput checks required fields and comment constraints.
func validateModifyInput(input CallsModifyInput) error {
	if input.File == "" {
		return fmt.Errorf("--file is required")
	}
	if input.Reviewer == "" {
		return fmt.Errorf("--reviewer is required")
	}
	if input.Filter == "" {
		return fmt.Errorf("--filter is required")
	}
	if input.Segment == "" {
		return fmt.Errorf("--segment is required")
	}
	if len(input.Comment) > 140 {
		return fmt.Errorf("--comment must be 140 characters or less")
	}
	for i, r := range input.Comment {
		if r > 127 {
			return fmt.Errorf("--comment must be ASCII only (non-ASCII at position %d)", i)
		}
	}
	return nil
}

// resolveSpecies parses species+calltype from the input species string.
// If input species is empty, keeps the existing label values.
func resolveSpecies(inputSpecies string, label *utils.Label) (species, callType string) {
	if inputSpecies == "" {
		return label.Species, label.CallType
	}
	if before, after, ok := strings.Cut(inputSpecies, "+"); ok {
		return before, after
	}
	return inputSpecies, ""
}

// hasModifyChanges checks whether any field would actually change.
func hasModifyChanges(newSpecies, newCallType string, input CallsModifyInput, label *utils.Label) bool {
	if newSpecies != label.Species || newCallType != label.CallType {
		return true
	}
	if input.Certainty != label.Certainty {
		return true
	}
	if input.Bookmark != nil && *input.Bookmark != label.Bookmark {
		return true
	}
	if input.Comment != "" {
		return true
	}
	return false
}

// applyLabelChanges updates the label and data file, populating the output.
func applyLabelChanges(label *utils.Label, dataFile *utils.DataFile, input CallsModifyInput, newSpecies, newCallType string, output *CallsModifyOutput) error {
	dataFile.Meta.Reviewer = input.Reviewer

	label.Species = newSpecies
	label.CallType = newCallType
	output.Species = newSpecies
	output.CallType = newCallType

	label.Certainty = input.Certainty
	output.Certainty = input.Certainty

	if input.Bookmark != nil && *input.Bookmark != label.Bookmark {
		label.Bookmark = *input.Bookmark
		output.Bookmark = input.Bookmark
	}

	if input.Comment != "" {
		var newComment string
		if label.Comment != "" {
			newComment = label.Comment + " | " + input.Comment
		} else {
			newComment = input.Comment
		}
		if len(newComment) > 140 {
			return fmt.Errorf("combined comment exceeds 140 characters (%d)", len(newComment))
		}
		label.Comment = newComment
		output.Comment = newComment
	}

	return nil
}

// CallsModify modifies a label in a .data file
func CallsModify(input CallsModifyInput) (CallsModifyOutput, error) {
	var output CallsModifyOutput

	if err := validateModifyInput(input); err != nil {
		output.Error = err.Error()
		return output, err
	}

	startTime, endTime, err := parseSegmentRange(input.Segment)
	if err != nil {
		output.Error = err.Error()
		return output, err
	}

	output.File = input.File
	output.SegmentStart = startTime
	output.SegmentEnd = endTime

	if _, err := os.Stat(input.File); os.IsNotExist(err) {
		output.Error = fmt.Sprintf("File not found: %s", input.File)
		return output, fmt.Errorf("%s", output.Error)
	}

	dataFile, err := utils.ParseDataFile(input.File)
	if err != nil {
		output.Error = fmt.Sprintf("Failed to parse file: %v", err)
		return output, fmt.Errorf("%s", output.Error)
	}

	segment := findSegment(dataFile.Segments, startTime, endTime, input.Filter)
	if segment == nil {
		output.Error = fmt.Sprintf("No segment found matching time range %d-%d", startTime, endTime)
		return output, fmt.Errorf("%s", output.Error)
	}

	targetLabel := findLabelByFilter(segment, input.Filter)
	if targetLabel == nil {
		output.Error = fmt.Sprintf("No label found with filter '%s' in segment %d-%d", input.Filter, startTime, endTime)
		return output, fmt.Errorf("%s", output.Error)
	}

	output.PreviousValue = formatLabel(targetLabel)

	newSpecies, newCallType := resolveSpecies(input.Species, targetLabel)

	if !hasModifyChanges(newSpecies, newCallType, input, targetLabel) {
		output.Error = "No changes needed: all values already match"
		return output, fmt.Errorf("%s", output.Error)
	}

	if err := applyLabelChanges(targetLabel, dataFile, input, newSpecies, newCallType, &output); err != nil {
		output.Error = err.Error()
		return output, err
	}

	if err := dataFile.Write(input.File); err != nil {
		output.Error = fmt.Sprintf("Failed to save file: %v", err)
		return output, fmt.Errorf("%s", output.Error)
	}

	return output, nil
}

// findLabelByFilter finds the first label matching the given filter in a segment.
func findLabelByFilter(segment *utils.Segment, filter string) *utils.Label {
	for _, label := range segment.Labels {
		if label.Filter == filter {
			return label
		}
	}
	return nil
}

// parseSegmentRange parses "12-15" format into start and end integers
func parseSegmentRange(s string) (int, int, error) {
	parts := strings.Split(s, "-")
	if len(parts) != 2 {
		return 0, 0, fmt.Errorf("invalid segment format: %s (expected start-end, e.g., 12-15)", s)
	}

	var start, end int
	if _, err := fmt.Sscanf(parts[0], "%d", &start); err != nil {
		return 0, 0, fmt.Errorf("invalid start time: %s", parts[0])
	}
	if _, err := fmt.Sscanf(parts[1], "%d", &end); err != nil {
		return 0, 0, fmt.Errorf("invalid end time: %s", parts[1])
	}

	if start < 0 || end < 0 {
		return 0, 0, fmt.Errorf("times must be non-negative")
	}
	if start >= end {
		return 0, 0, fmt.Errorf("start time must be less than end time")
	}

	return start, end, nil
}

// findSegment finds a segment matching the time range using floor/ceil matching.
// It also checks that the segment contains a label with the specified filter,
// so that duplicate segments (same time range, different filters) are resolved correctly.
func findSegment(segments []*utils.Segment, startTime, endTime int, filter string) *utils.Segment {
	for _, seg := range segments {
		segStart := int(math.Floor(seg.StartTime))
		segEnd := int(math.Ceil(seg.EndTime))
		if segEnd == segStart {
			segEnd = segStart + 1 // minimum 1 second
		}
		if segStart == startTime && segEnd == endTime {
			for _, label := range seg.Labels {
				if label.Filter == filter {
					return seg
				}
			}
		}
	}
	return nil
}

// formatLabel formats a label for display
func formatLabel(label *utils.Label) string {
	result := label.Species
	if label.CallType != "" {
		result += "+" + label.CallType
	}
	result += fmt.Sprintf(" (%d%%)", label.Certainty)
	return result
}