package utils

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

// SpeciesMapping maps .data species/calltype names to DB labels
type SpeciesMapping struct {
	Species   string            `json:"species"`
	Calltypes map[string]string `json:"calltypes,omitempty"`
}

// MappingFile represents the complete mapping file structure
// Key is the .data file species name
type MappingFile map[string]SpeciesMapping

// LoadMappingFile loads and parses a mapping JSON file
func LoadMappingFile(path string) (MappingFile, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read mapping file: %w", err)
	}

	var mapping MappingFile
	if err := json.Unmarshal(data, &mapping); err != nil {
		return nil, fmt.Errorf("failed to parse mapping JSON: %w", err)
	}

	// Validate non-empty
	if len(mapping) == 0 {
		return nil, fmt.Errorf("mapping file is empty")
	}

	// Validate each entry has species
	for dataSpecies, sm := range mapping {
		if sm.Species == "" {
			return nil, fmt.Errorf("mapping entry '%s' has empty species field", dataSpecies)
		}
	}

	return mapping, nil
}

// MappingValidationResult contains validation errors for a mapping
type MappingValidationResult struct {
	MissingSpecies   []string          // .data species not in mapping
	MissingDBSpecies []string          // mapped species not in DB
	MissingCalltypes map[string]string // "dataSpecies/dataCalltype" -> "dbSpecies/dbCalltype"
}

// HasErrors returns true if any validation errors exist
func (r MappingValidationResult) HasErrors() bool {
	return len(r.MissingSpecies) > 0 ||
		len(r.MissingDBSpecies) > 0 ||
		len(r.MissingCalltypes) > 0
}

// Error returns a formatted error message
func (r MappingValidationResult) Error() string {
	var parts []string

	if len(r.MissingSpecies) > 0 {
		parts = append(parts, fmt.Sprintf("species in .data but not in mapping: [%s]",
			strings.Join(r.MissingSpecies, ", ")))
	}

	if len(r.MissingDBSpecies) > 0 {
		parts = append(parts, fmt.Sprintf("mapped species not found in DB: [%s]",
			strings.Join(r.MissingDBSpecies, ", ")))
	}

	if len(r.MissingCalltypes) > 0 {
		var ctErrors []string
		for k, v := range r.MissingCalltypes {
			ctErrors = append(ctErrors, fmt.Sprintf("%s->%s", k, v))
		}
		sort.Strings(ctErrors)
		parts = append(parts, fmt.Sprintf("calltypes not found in DB: [%s]",
			strings.Join(ctErrors, ", ")))
	}

	return strings.Join(parts, "; ")
}

// ValidateMappingAgainstDB validates that all mapped species and calltypes exist in the database
// Also validates that the mapping covers all species/calltypes found in .data files
func ValidateMappingAgainstDB(
	queryer DB,
	mapping MappingFile,
	dataSpeciesSet map[string]bool,
	dataCalltypes map[string]map[string]bool, // species -> calltype -> true
) (MappingValidationResult, error) {
	result := MappingValidationResult{
		MissingSpecies:   make([]string, 0),
		MissingDBSpecies: make([]string, 0),
		MissingCalltypes: make(map[string]string),
	}

	// Check all .data species are in mapping
	for species := range dataSpeciesSet {
		if _, exists := mapping[species]; !exists {
			result.MissingSpecies = append(result.MissingSpecies, species)
		}
	}
	sort.Strings(result.MissingSpecies)

	// Collect all mapped species and calltypes
	mappedSpeciesSet, mappedCalltypes := collectMappedLabels(mapping, dataCalltypes)

	// Validate species exist in DB
	if err := validateMappedSpecies(queryer, mappedSpeciesSet, &result); err != nil {
		return result, err
	}

	// Validate calltypes exist in DB
	if err := validateMappedCalltypes(queryer, mappedCalltypes, &result); err != nil {
		return result, err
	}

	return result, nil
}

// collectMappedLabels builds sets of mapped species and calltype labels
func collectMappedLabels(mapping MappingFile, dataCalltypes map[string]map[string]bool) (map[string]bool, map[string]map[string]string) {
	mappedSpeciesSet := make(map[string]bool)
	mappedCalltypes := make(map[string]map[string]string) // dbSpecies -> dbCalltype -> dataCalltype

	for _, sm := range mapping {
		// Skip sentinel values — they are never looked up in the DB
		if sm.Species == MappingNegative || sm.Species == MappingIgnore {
			continue
		}

		mappedSpeciesSet[sm.Species] = true

		// Track calltype mappings
		if len(sm.Calltypes) > 0 {
			if mappedCalltypes[sm.Species] == nil {
				mappedCalltypes[sm.Species] = make(map[string]string)
			}
			for dataCT, dbCT := range sm.Calltypes {
				mappedCalltypes[sm.Species][dbCT] = dataCT
			}
		}
	}

	// Also collect unmapped calltypes (where .data calltype = DB calltype)
	for dataSpecies, ctSet := range dataCalltypes {
		sm, exists := mapping[dataSpecies]
		if !exists {
			continue // Already reported as missing species
		}
		dbSpecies := sm.Species

		for dataCT := range ctSet {
			// If no explicit mapping, assume dataCT == dbCT
			dbCT := dataCT
			if sm.Calltypes != nil {
				if mapped, ok := sm.Calltypes[dataCT]; ok {
					dbCT = mapped
				}
			}

			if mappedCalltypes[dbSpecies] == nil {
				mappedCalltypes[dbSpecies] = make(map[string]string)
			}
			mappedCalltypes[dbSpecies][dbCT] = dataCT
		}
	}

	return mappedSpeciesSet, mappedCalltypes
}

// validateMappedSpecies checks that all mapped species exist in the database
func validateMappedSpecies(queryer DB, mappedSpeciesSet map[string]bool, result *MappingValidationResult) error {
	speciesLabels := make([]string, 0, len(mappedSpeciesSet))
	for s := range mappedSpeciesSet {
		speciesLabels = append(speciesLabels, s)
	}
	sort.Strings(speciesLabels)

	if len(speciesLabels) == 0 {
		return nil
	}

	query := `SELECT label FROM species WHERE label IN (` + Placeholders(len(speciesLabels)) + `) AND active = true`
	args := make([]any, len(speciesLabels))
	for i, s := range speciesLabels {
		args[i] = s
	}

	rows, err := queryer.Query(query, args...)
	if err != nil {
		return fmt.Errorf("failed to query species: %w", err)
	}
	defer rows.Close()

	foundSpecies := make(map[string]bool)
	for rows.Next() {
		var label string
		if err := rows.Scan(&label); err == nil {
			foundSpecies[label] = true
		}
	}

	for _, s := range speciesLabels {
		if !foundSpecies[s] {
			result.MissingDBSpecies = append(result.MissingDBSpecies, s)
		}
	}
	return nil
}

// validateMappedCalltypes checks that all mapped calltypes exist in the database
func validateMappedCalltypes(queryer DB, mappedCalltypes map[string]map[string]string, result *MappingValidationResult) error {
	for dbSpecies, ctMap := range mappedCalltypes {
		if len(ctMap) == 0 {
			continue
		}

		ctLabels := make([]string, 0, len(ctMap))
		for dbCT := range ctMap {
			ctLabels = append(ctLabels, dbCT)
		}
		sort.Strings(ctLabels)

		query := `
			SELECT ct.label
			FROM call_type ct
			JOIN species s ON ct.species_id = s.id
			WHERE s.label = ? AND ct.label IN (` + Placeholders(len(ctLabels)) + `) AND ct.active = true`
		args := make([]any, 1+len(ctLabels))
		args[0] = dbSpecies
		for i, ct := range ctLabels {
			args[1+i] = ct
		}

		rows, err := queryer.Query(query, args...)
		if err != nil {
			return fmt.Errorf("failed to query calltypes for species %s: %w", dbSpecies, err)
		}
		defer rows.Close()

		foundCT := make(map[string]bool)
		for rows.Next() {
			var label string
			if err := rows.Scan(&label); err == nil {
				foundCT[label] = true
			}
		}

		for dbCT, dataCT := range ctMap {
			if !foundCT[dbCT] {
				key := fmt.Sprintf("%s/%s", dbSpecies, dataCT)
				value := fmt.Sprintf("%s/%s", dbSpecies, dbCT)
				result.MissingCalltypes[key] = value
			}
		}
	}
	return nil
}

// GetDBSpecies returns the DB species label for a .data species
func (m MappingFile) GetDBSpecies(dataSpecies string) (string, bool) {
	sm, exists := m[dataSpecies]
	if !exists {
		return "", false
	}
	return sm.Species, true
}

// GetDBCalltype returns the DB calltype label for a .data species/calltype
// Returns the dataCalltype unchanged if no mapping exists
func (m MappingFile) GetDBCalltype(dataSpecies, dataCalltype string) string {
	sm, exists := m[dataSpecies]
	if !exists || sm.Calltypes == nil {
		return dataCalltype
	}

	if dbCT, ok := sm.Calltypes[dataCalltype]; ok {
		return dbCT
	}
	return dataCalltype
}

// Mapping sentinels: special values for the SpeciesMapping.Species field.
//
// MappingNegative marks a .data species as "confirmed empty" (Noise-equivalent):
// segments matching this name are treated as negative evidence — clips overlapping
// them emit an all-zero row when no positive species also overlaps.
//
// MappingIgnore marks a .data species as "ignored entirely": segments matching
// this name neither label clips nor block them.
const (
	MappingNegative = "__NEGATIVE__"
	MappingIgnore   = "__IGNORE__"
)

// MappingKind describes how a .data species should be treated.
type MappingKind int

const (
	MappingReal MappingKind = iota
	MappingNeg
	MappingIgn
)

// Classify returns the canonical class name and kind for a .data species.
// ok is false if dataSpecies is not present in the mapping.
// For MappingNeg and MappingIgn the canonical string is empty.
func (m MappingFile) Classify(dataSpecies string) (canonical string, kind MappingKind, ok bool) {
	sm, exists := m[dataSpecies]
	if !exists {
		return "", MappingReal, false
	}
	switch sm.Species {
	case MappingNegative:
		return "", MappingNeg, true
	case MappingIgnore:
		return "", MappingIgn, true
	default:
		return sm.Species, MappingReal, true
	}
}

// ValidateCoversSpecies returns the sorted list of species in speciesSet that
// are missing from the mapping. Empty result means full coverage.
func (m MappingFile) ValidateCoversSpecies(speciesSet map[string]bool) []string {
	missing := make([]string, 0)
	for s := range speciesSet {
		if _, exists := m[s]; !exists {
			missing = append(missing, s)
		}
	}
	sort.Strings(missing)
	return missing
}

// Classes returns the sorted unique non-sentinel canonical class names from the mapping.
// Used to build the CSV column header for clip-labels.
func (m MappingFile) Classes() []string {
	set := make(map[string]bool)
	for _, sm := range m {
		switch sm.Species {
		case MappingNegative, MappingIgnore, "":
			continue
		default:
			set[sm.Species] = true
		}
	}
	out := make([]string, 0, len(set))
	for s := range set {
		out = append(out, s)
	}
	sort.Strings(out)
	return out
}