Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

calls_remove.go
package cmd

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

	"skraak/config"
	"skraak/tools/calls"
)

func printRemoveUsage() {
	fmt.Fprintf(os.Stderr, "Usage: skraak calls remove [options]\n\n")
	fmt.Fprintf(os.Stderr, "Remove a label from a .data file.\n\n")
	fmt.Fprintf(os.Stderr, "Options:\n")
	fmt.Fprintf(os.Stderr, "  --file <path>        Path to .data file (required)\n")
	fmt.Fprintf(os.Stderr, "  --segment <st-en>    Time range, e.g., 12.3-15.7 or 12-15 (required)\n")
	fmt.Fprintf(os.Stderr, "  --frequency <lo-hi>  Frequency range, e.g., 200-4500 (optional; required if ambiguous)\n")
	fmt.Fprintf(os.Stderr, "  --species <name>     Species, optionally with calltype (required, e.g., Kiwi, Kiwi+Duet)\n")
	fmt.Fprintf(os.Stderr, "  --filter <name>      Filter name (default: Manual)\n")
	fmt.Fprintf(os.Stderr, "  --reviewer <name>    Reviewer name (default: from config)\n")
	fmt.Fprintf(os.Stderr, "\nBehavior:\n")
	fmt.Fprintf(os.Stderr, "  Removes the matching label. If the segment has no labels left, removes the segment.\n")
	fmt.Fprintf(os.Stderr, "  If the .data file has no segments left, removes the .data file.\n")
	fmt.Fprintf(os.Stderr, "  If --calltype is omitted, matches any calltype (errors if ambiguous).\n")
	fmt.Fprintf(os.Stderr, "  If --frequency is omitted, matches by time only (errors if multiple segments match).\n")
	fmt.Fprintf(os.Stderr, "\nExamples:\n")
	fmt.Fprintf(os.Stderr, "  # Remove a Kiwi label\n")
	fmt.Fprintf(os.Stderr, "  skraak calls remove --file recording.wav.data --segment 12-15 --species Kiwi\n\n")
	fmt.Fprintf(os.Stderr, "  # Remove a specific calltype\n")
	fmt.Fprintf(os.Stderr, "  skraak calls remove --file recording.wav.data --segment 12-15 --species Kiwi+Duet\n\n")
	fmt.Fprintf(os.Stderr, "  # Remove with frequency to disambiguate\n")
	fmt.Fprintf(os.Stderr, "  skraak calls remove --file recording.wav.data --segment 12-15 --frequency 200-4500 --species Kiwi\n")
}

// removeArgs holds parsed CLI arguments for the remove command.
type removeArgs struct {
	file      string
	segment   string
	frequency string
	species   string
	filter    string
	reviewer  string
}

// removeFlagHandler returns a flag handler for removeArgs.
func removeFlagHandler(arg string, ra *removeArgs) (func(string), bool, bool) {
	switch arg {
	case "--file":
		return func(v string) { ra.file = v }, false, true
	case "--segment":
		return func(v string) { ra.segment = v }, false, true
	case "--frequency":
		return func(v string) { ra.frequency = v }, false, true
	case "--species":
		return func(v string) { ra.species = v }, false, true
	case "--filter":
		return func(v string) { ra.filter = v }, false, true
	case "--reviewer":
		return func(v string) { ra.reviewer = v }, false, true
	default:
		return nil, false, false
	}
}

// parseRemoveArgs parses the command-line arguments for the remove subcommand.
func parseRemoveArgs(args []string) removeArgs {
	var ra removeArgs
	i := 0
	for i < len(args) {
		arg := args[i]
		if arg == "-h" || arg == "--help" {
			printRemoveUsage()
			os.Exit(0)
		}
		handler, isBool, handled := removeFlagHandler(arg, &ra)
		if !handled {
			handleUnknownFlag(arg)
			i++
			continue
		}
		if isBool {
			i++
		} else {
			handler(mustValue(args, &i, arg))
		}
	}
	return ra
}

// validateRemoveArgs checks required flags and resolves defaults.
func validateRemoveArgs(ra *removeArgs) error {
	missing := []string{}
	if ra.file == "" {
		missing = append(missing, "--file")
	}
	if ra.segment == "" {
		missing = append(missing, "--segment")
	}
	if ra.species == "" {
		missing = append(missing, "--species")
	}
	if len(missing) > 0 {
		printRemoveUsage()
		return fmt.Errorf("missing required flags: %s", strings.Join(missing, ", "))
	}

	// Apply defaults
	if ra.filter == "" {
		ra.filter = "Manual"
	}

	// Resolve reviewer from config if not specified
	if ra.reviewer == "" {
		cfg, cfgPath, err := config.LoadConfig()
		if err != nil {
			return fmt.Errorf("--reviewer not provided and config not found: %s", cfgPath)
		}
		if cfg.Classify.Reviewer == "" {
			return fmt.Errorf("--reviewer not provided and %s missing classify.reviewer", cfgPath)
		}
		ra.reviewer = cfg.Classify.Reviewer
	}

	return nil
}

// RunCallsRemove handles the "calls remove" subcommand
//
// JSON output schema:
//
//	{
//	  "file": string,
//	  "segment_start": float,
//	  "segment_end": float,
//	  "low_freq": float,
//	  "high_freq": float,
//	  "species": string,
//	  "calltype": string,
//	  "filter": string,
//	  "removed": string,         // "label", "segment", or "file"
//	  "error": string            // omitted if no error
//	}
func RunCallsRemove(args []string) error {
	ra := parseRemoveArgs(args)
	if err := validateRemoveArgs(&ra); err != nil {
		return err
	}

	input := calls.CallsRemoveInput{
		File:      ra.file,
		Segment:   ra.segment,
		Frequency: ra.frequency,
		Species:   ra.species,
		Filter:    ra.filter,
		Reviewer:  ra.reviewer,
	}

	result, err := calls.CallsRemove(input)
	if err != nil {
		return fmt.Errorf("remove: %s", result.Error)
	}

	data, _ := json.Marshal(result)
	fmt.Println(string(data))
	return nil
}