package tools

import (
	"encoding/csv"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"skraak/utils"
)

// CallsFromBirdaInput defines the input for the calls-from-birda tool
type CallsFromBirdaInput struct {
	Folder          string          `json:"folder"`
	File            string          `json:"file"`
	Delete          bool            `json:"delete"`
	ProgressHandler ProgressHandler `json:"-"` // Optional progress callback
}

// CallsFromBirdaOutput defines the output for the calls-from-birda tool
type CallsFromBirdaOutput struct {
	Calls            []ClusteredCall `json:"calls"`
	TotalCalls       int             `json:"total_calls"`
	SpeciesCount     map[string]int  `json:"species_count"`
	DataFilesWritten int             `json:"data_files_written"`
	DataFilesSkipped int             `json:"data_files_skipped"`
	FilesProcessed   int             `json:"files_processed"`
	FilesDeleted     int             `json:"files_deleted"`
	Filter           string          `json:"filter"`
	Error            *string         `json:"error,omitempty"`
}

// birdaSource implements CallSource for BirdNET results files
type birdaSource struct{}

func (birdaSource) Name() string { return "BirdNET" }

func (birdaSource) FindFiles(folder string) ([]string, error) {
	var files []string

	entries, err := os.ReadDir(folder)
	if err != nil {
		return nil, err
	}

	for _, entry := range entries {
		name := entry.Name()
		if strings.HasSuffix(name, ".BirdNET.results.csv") {
			files = append(files, filepath.Join(folder, name))
		}
	}

	return files, nil
}

func (birdaSource) ProcessFile(birdaFile string, cache *DirCache) ([]ClusteredCall, bool, bool, error) {
	return processBirdaFileCached(birdaFile, cache)
}

// CallsFromBirda processes BirdNET results files and writes .data files
func CallsFromBirda(input CallsFromBirdaInput) (CallsFromBirdaOutput, error) {
	src := birdaSource{}
	commonInput := CallsFromSourceInput(input)

	commonOutput, err := callsFromSource(src, commonInput)

	// Convert to Birda-specific output type
	var output CallsFromBirdaOutput
	output.Calls = commonOutput.Calls
	output.TotalCalls = commonOutput.TotalCalls
	output.SpeciesCount = commonOutput.SpeciesCount
	output.DataFilesWritten = commonOutput.DataFilesWritten
	output.DataFilesSkipped = commonOutput.DataFilesSkipped
	output.FilesProcessed = commonOutput.FilesProcessed
	output.FilesDeleted = commonOutput.FilesDeleted
	output.Filter = commonOutput.Filter
	output.Error = commonOutput.Error
	return output, err
}

// BirdNETDetection represents a single BirdNET detection
type BirdNETDetection struct {
	StartTime      float64
	EndTime        float64
	ScientificName string
	CommonName     string
	Confidence     float64
	WAVPath        string
}

// birdaColumnIndices holds the parsed column positions from a BirdNET CSV header.
type birdaColumnIndices struct {
	startIdx      int
	endIdx        int
	commonNameIdx int
	confidenceIdx int
	fileIdx       int
}

// parseBirdaCSVHeader reads the CSV header row and returns column indices.
func parseBirdaCSVHeader(reader *csv.Reader) (birdaColumnIndices, error) {
	header, err := reader.Read()
	if err != nil {
		return birdaColumnIndices{}, fmt.Errorf("failed to read header: %w", err)
	}

	idx := birdaColumnIndices{startIdx: -1, endIdx: -1, commonNameIdx: -1, confidenceIdx: -1, fileIdx: -1}
	for i, col := range header {
		col = strings.TrimPrefix(col, "\ufeff")
		switch col {
		case "Start (s)":
			idx.startIdx = i
		case "End (s)":
			idx.endIdx = i
		case "Common name":
			idx.commonNameIdx = i
		case "Confidence":
			idx.confidenceIdx = i
		case "File":
			idx.fileIdx = i
		}
	}

	if idx.startIdx == -1 || idx.endIdx == -1 || idx.commonNameIdx == -1 || idx.confidenceIdx == -1 {
		return birdaColumnIndices{}, fmt.Errorf("missing required columns in BirdNET file")
	}
	return idx, nil
}

// readBirdaDetections reads all detection records from a BirdNET CSV.
func readBirdaDetections(reader *csv.Reader, idx birdaColumnIndices) ([]BirdNETDetection, error) {
	var detections []BirdNETDetection
	for {
		record, err := reader.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("failed to read record: %w", err)
		}

		var det BirdNETDetection
		startTime, perr := strconv.ParseFloat(record[idx.startIdx], 64)
		if perr != nil {
			return nil, fmt.Errorf("failed to parse start time %q: %w", record[idx.startIdx], perr)
		}
		det.StartTime = startTime

		endTime, perr := strconv.ParseFloat(record[idx.endIdx], 64)
		if perr != nil {
			return nil, fmt.Errorf("failed to parse end time %q: %w", record[idx.endIdx], perr)
		}
		det.EndTime = endTime

		det.CommonName = record[idx.commonNameIdx]

		confidence, perr := strconv.ParseFloat(record[idx.confidenceIdx], 64)
		if perr != nil {
			return nil, fmt.Errorf("failed to parse confidence %q: %w", record[idx.confidenceIdx], perr)
		}
		det.Confidence = confidence

		if idx.fileIdx >= 0 && idx.fileIdx < len(record) {
			det.WAVPath = record[idx.fileIdx]
		}

		detections = append(detections, det)
	}
	return detections, nil
}

// resolveBirdaWAVPath finds the WAV file associated with a BirdNET results file.
func resolveBirdaWAVPath(birdaFile string, firstWAVPath string, cache *DirCache) string {
	if firstWAVPath != "" {
		if _, err := os.Stat(firstWAVPath); err == nil {
			return firstWAVPath
		}
	}

	dir := filepath.Dir(birdaFile)
	base := filepath.Base(birdaFile)
	baseName := strings.TrimSuffix(base, ".BirdNET.results.csv")

	if cache != nil {
		return cache.FindWAV(baseName)
	}
	return findWAVFile(dir, baseName)
}

// processBirdaFileCached processes a single BirdNET results file using a DirCache for WAV lookup
func processBirdaFileCached(birdaFile string, cache *DirCache) ([]ClusteredCall, bool, bool, error) {
	file, err := os.Open(birdaFile)
	if err != nil {
		return nil, false, false, fmt.Errorf("failed to open file: %w", err)
	}
	defer func() { _ = file.Close() }()

	reader := csv.NewReader(file)

	idx, err := parseBirdaCSVHeader(reader)
	if err != nil {
		return nil, false, false, err
	}

	detections, err := readBirdaDetections(reader, idx)
	if err != nil {
		return nil, false, false, err
	}
	if len(detections) == 0 {
		return nil, false, true, nil
	}

	wavPath := resolveBirdaWAVPath(birdaFile, detections[0].WAVPath, cache)
	if wavPath == "" {
		return nil, false, true, nil
	}

	sampleRate, duration, err := utils.ParseWAVHeaderMinimal(wavPath)
	if err != nil {
		return nil, false, true, nil
	}

	dataPath := wavPath + ".data"
	segments := buildBirdNETSegments(detections, sampleRate)

	meta := AviaNZMeta{Operator: "BirdNET", Duration: duration}
	reviewer := "None"
	meta.Reviewer = &reviewer

	if err := writeDotDataFileSafe(dataPath, segments, "BirdNET", meta); err != nil {
		return nil, false, false, err
	}

	var calls []ClusteredCall
	for _, det := range detections {
		calls = append(calls, ClusteredCall{
			File:      wavPath,
			StartTime: det.StartTime,
			EndTime:   det.EndTime,
			EbirdCode: det.CommonName,
			Segments:  1,
		})
	}

	return calls, true, false, nil
}

// buildBirdNETSegments converts BirdNET detections to AviaNZ segments
func buildBirdNETSegments(detections []BirdNETDetection, sampleRate int) []AviaNZSegment {
	var segments []AviaNZSegment

	for _, det := range detections {
		// Convert confidence (0.0-1.0) to certainty (0-100)
		certainty := min(max(int(det.Confidence*100), 0), 100)

		labels := []AviaNZLabel{
			{
				Species:   det.CommonName,
				Certainty: certainty,
				Filter:    "BirdNET",
			},
		}

		segment := AviaNZSegment{
			det.StartTime,
			det.EndTime,
			0,          // freq_low
			sampleRate, // freq_high (full band)
			labels,
		}
		segments = append(segments, segment)
	}

	return segments
}