package tools

import (
	"fmt"
	"os"

	"skraak/utils"
)

type CallsPropagateInput struct {
	File       string `json:"file"`
	FromFilter string `json:"from_filter"`
	ToFilter   string `json:"to_filter"`
	Species    string `json:"species"`
}

type CallsPropagateOutput struct {
	File             string              `json:"file"`
	FromFilter       string              `json:"from_filter"`
	ToFilter         string              `json:"to_filter"`
	Species          string              `json:"species"`
	FiltersMissing   bool                `json:"filters_missing,omitempty"`
	TargetsExamined  int                 `json:"targets_examined"`
	Propagated       int                 `json:"propagated"`
	SkippedNoOverlap int                 `json:"skipped_no_overlap"`
	SkippedConflict  int                 `json:"skipped_conflict"`
	Conflicts        []PropagateConflict `json:"conflicts,omitempty"`
	Changes          []PropagateChange   `json:"changes,omitempty"`
	Error            string              `json:"error,omitempty"`
}

type CallsPropagateFolderInput struct {
	Folder     string `json:"folder"`
	FromFilter string `json:"from_filter"`
	ToFilter   string `json:"to_filter"`
	Species    string `json:"species"`
}

type CallsPropagateFolderOutput struct {
	Folder               string                 `json:"folder"`
	FromFilter           string                 `json:"from_filter"`
	ToFilter             string                 `json:"to_filter"`
	Species              string                 `json:"species"`
	FilesTotal           int                    `json:"files_total"`
	FilesWithBothFilters int                    `json:"files_with_both_filters"`
	FilesSkippedNoFilter int                    `json:"files_skipped_no_filter"`
	FilesChanged         int                    `json:"files_changed"`
	FilesErrored         int                    `json:"files_errored"`
	TargetsExamined      int                    `json:"targets_examined"`
	Propagated           int                    `json:"propagated"`
	SkippedNoOverlap     int                    `json:"skipped_no_overlap"`
	SkippedConflict      int                    `json:"skipped_conflict"`
	Conflicts            []PropagateConflict    `json:"conflicts,omitempty"`
	Errors               []CallsPropagateOutput `json:"errors,omitempty"`
	Error                string                 `json:"error,omitempty"`
}

type PropagateConflict struct {
	File           string                  `json:"file,omitempty"`
	TargetStart    float64                 `json:"target_start"`
	TargetEnd      float64                 `json:"target_end"`
	TargetCallType string                  `json:"target_calltype,omitempty"`
	SourceChoices  []PropagateSourceChoice `json:"source_choices"`
}

type PropagateSourceChoice struct {
	Start    float64 `json:"start"`
	End      float64 `json:"end"`
	Species  string  `json:"species"`
	CallType string  `json:"calltype,omitempty"`
}

type PropagateChange struct {
	TargetStart   float64 `json:"target_start"`
	TargetEnd     float64 `json:"target_end"`
	PrevSpecies   string  `json:"prev_species"`
	PrevCallType  string  `json:"prev_calltype,omitempty"`
	PrevCertainty int     `json:"prev_certainty"`
	NewSpecies    string  `json:"new_species"`
	NewCallType   string  `json:"new_calltype,omitempty"`
	NewCertainty  int     `json:"new_certainty"`
}

// CallsPropagate copies verified classifications (certainty==100) from one filter's
// segments to overlapping target segments of another filter, within a single .data file.
// Target labels with certainty==70 (ML-unverified) or certainty==0 (Don't Know / Noise)
// are updated — targets at certainty==100 (human-verified) and certainty==90 (already
// propagated) are left alone. Only source labels matching --species are considered.
// Propagated target labels are set to certainty=90 and file reviewer is set to "Skraak".
func CallsPropagate(input CallsPropagateInput) (CallsPropagateOutput, error) {
	output := CallsPropagateOutput{
		File:       input.File,
		FromFilter: input.FromFilter,
		ToFilter:   input.ToFilter,
		Species:    input.Species,
	}

	if err := validatePropagateInput(&output, input); err != nil {
		return output, err
	}

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

	// Fast path: skip files that don't contain both filters at all.
	if !hasBothFilters(df, input.FromFilter, input.ToFilter) {
		output.FiltersMissing = true
		return output, nil
	}

	sources := collectPropagateSources(df, input.FromFilter, input.Species)

	propagateTargets(df, sources, input, &output)

	if output.Propagated > 0 {
		df.Meta.Reviewer = "Skraak"
		if err := df.Write(input.File); err != nil {
			output.Error = fmt.Sprintf("write %s: %v", input.File, err)
			return output, fmt.Errorf("%s", output.Error)
		}
	}

	return output, nil
}

// validatePropagateInput checks required fields and file existence
func validatePropagateInput(output *CallsPropagateOutput, input CallsPropagateInput) error {
	checks := []struct {
		val string
		msg string
	}{
		{input.File, "--file is required"},
		{input.FromFilter, "--from is required"},
		{input.ToFilter, "--to is required"},
		{input.Species, "--species is required"},
	}
	for _, c := range checks {
		if c.val == "" {
			output.Error = c.msg
			return fmt.Errorf("%s", c.msg)
		}
	}
	if input.FromFilter == input.ToFilter {
		output.Error = "--from and --to must differ"
		return fmt.Errorf("%s", output.Error)
	}
	if _, err := os.Stat(input.File); os.IsNotExist(err) {
		output.Error = fmt.Sprintf("file not found: %s", input.File)
		return fmt.Errorf("%s", output.Error)
	}
	return nil
}

// hasBothFilters checks whether the data file contains both from and to filters
func hasBothFilters(df *utils.DataFile, fromFilter, toFilter string) bool {
	hasFrom, hasTo := false, false
	for _, seg := range df.Segments {
		for _, lbl := range seg.Labels {
			if lbl.Filter == fromFilter {
				hasFrom = true
			}
			if lbl.Filter == toFilter {
				hasTo = true
			}
			if hasFrom && hasTo {
				return true
			}
		}
	}
	return false
}

// sourceRef pairs a segment with its matching source label
type sourceRef struct {
	seg   *utils.Segment
	label *utils.Label
}

// collectPropagateSources gathers verified source labels (certainty==100) for the given filter/species
func collectPropagateSources(df *utils.DataFile, fromFilter, species string) []sourceRef {
	var sources []sourceRef
	for _, seg := range df.Segments {
		for _, lbl := range seg.Labels {
			if lbl.Filter == fromFilter && lbl.Species == species && lbl.Certainty == 100 {
				sources = append(sources, sourceRef{seg: seg, label: lbl})
				break
			}
		}
	}
	return sources
}

// propagateTargets iterates target segments, finds overlapping sources, and applies agreed classifications
func propagateTargets(df *utils.DataFile, sources []sourceRef, input CallsPropagateInput, output *CallsPropagateOutput) {
	for _, tSeg := range df.Segments {
		toLabel := findUpdatableTargetLabel(tSeg.Labels, input.ToFilter)
		if toLabel == nil {
			continue
		}
		output.TargetsExamined++

		overlaps := findOverlappingSources(sources, tSeg)
		if len(overlaps) == 0 {
			output.SkippedNoOverlap++
			continue
		}

		agreedCallType, conflict := resolveCallType(overlaps)
		if conflict {
			output.SkippedConflict++
			output.Conflicts = append(output.Conflicts, buildConflictRecord(tSeg, toLabel, overlaps))
			continue
		}

		applyPropagation(toLabel, input.Species, agreedCallType, tSeg, output)
	}
}

// findUpdatableTargetLabel finds a target label with certainty 70 or 0 for the given filter
func findUpdatableTargetLabel(labels []*utils.Label, toFilter string) *utils.Label {
	for _, lbl := range labels {
		if lbl.Filter == toFilter && (lbl.Certainty == 70 || lbl.Certainty == 0) {
			return lbl
		}
	}
	return nil
}

// findOverlappingSources returns sources whose segments overlap with the target segment
func findOverlappingSources(sources []sourceRef, tSeg *utils.Segment) []sourceRef {
	var overlaps []sourceRef
	for _, s := range sources {
		if s.seg.StartTime < tSeg.EndTime && tSeg.StartTime < s.seg.EndTime {
			overlaps = append(overlaps, s)
		}
	}
	return overlaps
}

// resolveCallType checks if all overlapping sources agree on a call type.
// Returns the agreed call type and whether there is a conflict.
func resolveCallType(overlaps []sourceRef) (string, bool) {
	agreedCallType := overlaps[0].label.CallType
	for _, s := range overlaps[1:] {
		if s.label.CallType != agreedCallType {
			return "", true
		}
	}
	return agreedCallType, false
}

// buildConflictRecord creates a PropagateConflict from overlapping disagreeing sources
func buildConflictRecord(tSeg *utils.Segment, toLabel *utils.Label, overlaps []sourceRef) PropagateConflict {
	choices := make([]PropagateSourceChoice, 0, len(overlaps))
	for _, s := range overlaps {
		choices = append(choices, PropagateSourceChoice{
			Start:    s.seg.StartTime,
			End:      s.seg.EndTime,
			Species:  s.label.Species,
			CallType: s.label.CallType,
		})
	}
	return PropagateConflict{
		TargetStart:    tSeg.StartTime,
		TargetEnd:      tSeg.EndTime,
		TargetCallType: toLabel.CallType,
		SourceChoices:  choices,
	}
}

// applyPropagation updates the target label and records the change
func applyPropagation(toLabel *utils.Label, species, callType string, tSeg *utils.Segment, output *CallsPropagateOutput) {
	change := PropagateChange{
		TargetStart:   tSeg.StartTime,
		TargetEnd:     tSeg.EndTime,
		PrevSpecies:   toLabel.Species,
		PrevCallType:  toLabel.CallType,
		PrevCertainty: toLabel.Certainty,
		NewSpecies:    species,
		NewCallType:   callType,
		NewCertainty:  90,
	}

	toLabel.Species = species
	toLabel.CallType = callType
	toLabel.Certainty = 90

	output.Propagated++
	output.Changes = append(output.Changes, change)
}

// CallsPropagateFolder runs CallsPropagate against every .data file in a folder,
// aggregating counts. Files that do not contain both --from and --to filters are
// skipped silently (counted as files_skipped_no_filter). Parse/write errors on
// individual files are collected in Errors; they don't abort the run.
func CallsPropagateFolder(input CallsPropagateFolderInput) (CallsPropagateFolderOutput, error) {
	output := CallsPropagateFolderOutput{
		Folder:     input.Folder,
		FromFilter: input.FromFilter,
		ToFilter:   input.ToFilter,
		Species:    input.Species,
	}

	if input.Folder == "" {
		output.Error = "--folder is required"
		return output, fmt.Errorf("%s", output.Error)
	}
	if input.FromFilter == "" {
		output.Error = "--from is required"
		return output, fmt.Errorf("%s", output.Error)
	}
	if input.ToFilter == "" {
		output.Error = "--to is required"
		return output, fmt.Errorf("%s", output.Error)
	}
	if input.Species == "" {
		output.Error = "--species is required"
		return output, fmt.Errorf("%s", output.Error)
	}
	if input.FromFilter == input.ToFilter {
		output.Error = "--from and --to must differ"
		return output, fmt.Errorf("%s", output.Error)
	}

	info, err := os.Stat(input.Folder)
	if err != nil {
		output.Error = fmt.Sprintf("folder not found: %s", input.Folder)
		return output, fmt.Errorf("%s", output.Error)
	}
	if !info.IsDir() {
		output.Error = fmt.Sprintf("not a directory: %s", input.Folder)
		return output, fmt.Errorf("%s", output.Error)
	}

	files, err := utils.FindDataFiles(input.Folder)
	if err != nil {
		output.Error = fmt.Sprintf("list .data files: %v", err)
		return output, fmt.Errorf("%s", output.Error)
	}
	output.FilesTotal = len(files)

	for _, f := range files {
		fileOut, err := CallsPropagate(CallsPropagateInput{
			File:       f,
			FromFilter: input.FromFilter,
			ToFilter:   input.ToFilter,
			Species:    input.Species,
		})
		if err != nil {
			output.FilesErrored++
			output.Errors = append(output.Errors, fileOut)
			continue
		}
		if fileOut.FiltersMissing {
			output.FilesSkippedNoFilter++
			continue
		}
		output.FilesWithBothFilters++
		output.TargetsExamined += fileOut.TargetsExamined
		output.Propagated += fileOut.Propagated
		output.SkippedNoOverlap += fileOut.SkippedNoOverlap
		output.SkippedConflict += fileOut.SkippedConflict
		if fileOut.Propagated > 0 {
			output.FilesChanged++
		}
		for _, c := range fileOut.Conflicts {
			c.File = f
			output.Conflicts = append(output.Conflicts, c)
		}
	}

	return output, nil
}