// Command clipstudio indexes a library of SessionForge terminal recordings,
// finds the credentials somebody typed on camera, writes redacted copies that
// are safe to share, and enforces a retention policy that archives old clips
// without ever deleting one. Part of the Techlosoft "Screen Studio Lite" line.
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
	"time"
)

const (
	appName      = "clipstudio"
	appVersion   = "1.0.0"
	castVersion  = 1
	indexVersion = 1
	maxCastLine  = 8 * 1024 * 1024
	rfc3339      = time.RFC3339
)

// ---------------------------------------------------------------------------
// Shared Techlosoft UX helpers (identical across the tool family)
// ---------------------------------------------------------------------------

func reorderFlags(args []string, valueFlags map[string]bool) []string {
	var flags, positional []string
	for i := 0; i < len(args); i++ {
		a := args[i]
		name := strings.TrimLeft(a, "-")
		if strings.HasPrefix(a, "-") && valueFlags[name] {
			flags = append(flags, a)
			if i+1 < len(args) {
				i++
				flags = append(flags, args[i])
			}
			continue
		}
		if strings.HasPrefix(a, "-") {
			flags = append(flags, a)
			continue
		}
		positional = append(positional, a)
	}
	return append(flags, positional...)
}

func humanBytes(n int64) string {
	const unit = 1024
	if n < unit {
		return fmt.Sprintf("%d B", n)
	}
	div, exp := int64(unit), 0
	for x := n / unit; x >= unit; x /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}

// valueFlags lists every flag that consumes the following argument, so that
// flags can be written after positional arguments and still parse.
var valueFlags = map[string]bool{
	"lib": true, "l": true,
	"index": true, "i": true,
	"out": true, "o": true,
	"out-dir":    true,
	"older-than": true,
	"archive":    true,
	"ledger":     true,
}

func nowUTC() time.Time { return time.Now().UTC() }

func emitJSON(v any) {
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(v); err != nil {
		fail("cannot write JSON: %v", err)
	}
}

func newFlagSet(name string) *flag.FlagSet {
	fs := flag.NewFlagSet(name, flag.ContinueOnError)
	fs.SetOutput(os.Stderr)
	fs.Usage = usage
	return fs
}

func fail(format string, args ...any) {
	fmt.Fprintf(os.Stderr, "%s: %s\n", appName, fmt.Sprintf(format, args...))
	os.Exit(1)
}

func usageErr(format string, args ...any) {
	fmt.Fprintf(os.Stderr, "%s: %s\n\n", appName, fmt.Sprintf(format, args...))
	usage()
	os.Exit(1)
}

// ---------------------------------------------------------------------------
// Usage
// ---------------------------------------------------------------------------

const helpText = `clipstudio - clip library indexing, secret scanning and redaction
             for SessionForge terminal recordings (Techlosoft Screen Studio Lite)

USAGE
  clipstudio index  --lib <dir> [--index <file.json>] [--json]
  clipstudio list   (--index <file.json> | --lib <dir>) [--json]
  clipstudio search <term> (--index <file.json> | --lib <dir>) [--regexp] [--json]
  clipstudio scan   (<cast.jsonl> ... | --lib <dir>) [--json]
  clipstudio redact <cast.jsonl> --out <file.jsonl> [--force] [--json]
  clipstudio redact --lib <dir> --out-dir <dir> [--force] [--json]
  clipstudio retain --lib <dir> --older-than <age> --ledger <file.jsonl>
                    [--archive <dir>] [--apply] [--json]
  clipstudio help | -h | --help
  clipstudio version

COMMANDS
  index    Walk a library of cast files, parse each header (duration,
           dimensions, recorded-at, title), compute the SHA-256 of every file
           and build a library index. Written to --index if given.
  list     List the indexed clips, sorted by path.
  search   Find clips by term. Matches the recording's TEXT CONTENT as well as
           its filename, title and recorded command. The transcript is redacted
           before matching, so search can never surface a credential.
  scan     Reconstruct each recording's text stream and report the credentials
           in it: detector, severity, entropy, timestamp in the recording and a
           MASKED excerpt. The secret itself is never printed, in any mode.
  redact   Write a NEW cast with every finding replaced by a same-length mask.
           Event timing is byte-for-byte identical, the original file is opened
           read-only, and the output is re-scanned and asserted clean.
  retain   Report which clips a retention policy would archive. Archiving MOVES
           files into an archive directory - nothing is ever deleted. Dry run
           by default; --apply performs the moves. Every run appends one JSON
           line to --ledger.

FLAGS
  --lib <dir>          Clip library directory. *.jsonl and *.cast are indexed;
                       the _archive directory and dot-directories are skipped.
  --index <file.json>  Library index to write (index) or read (list, search).
  --out <file.jsonl>   Redacted output for a single input cast.
  --out-dir <dir>      Redacted output directory for --lib or several inputs.
  --force              Overwrite an existing redaction output.
  --regexp             Treat the search term as a regular expression.
  --older-than <age>   Retention age: 90d, 12h, 6w, 1y, 30m, 45s. A unit is
                       required - a bare number is rejected as ambiguous.
  --archive <dir>      Archive destination. Default <lib>/_archive.
  --ledger <file>      Append-only JSON-lines audit ledger. Required by retain.
  --apply              Perform the archive moves. Without it, retain is a dry
                       run that touches nothing.
  --json               Machine-readable output. Available on every reporting
                       subcommand. Secrets are masked in JSON too.

Short forms -l, -i and -o are accepted for --lib, --index and --out.
Flags may appear before or after positional arguments; either order works.

DETECTORS
  private-key-pem   aws-access-key-id  aws-secret-access-key  github-token
  google-api-key    slack-token        jwt                    bearer-token
  connection-string-password           secret-assignment      high-entropy-string

  The entropy detector reports strings of at least 20 characters whose Shannon
  entropy is at least 4.0 bits/char with mixed character classes, or at least
  32 characters at 4.5 bits/char. A de-noising pass discards UUIDs, digests in
  hash-like contexts, base64-encoded plain text and all-numeric tokens.

EXAMPLES
  clipstudio index  --lib ./clips --index clips.json
  clipstudio search "deploy" --index clips.json
  clipstudio scan   --lib ./clips --json
  clipstudio redact ./clips/deploy.jsonl --out ./share/deploy.jsonl
  clipstudio redact --lib ./clips --out-dir ./share
  clipstudio retain --lib ./clips --older-than 90d --ledger retention.jsonl
  clipstudio retain --lib ./clips --older-than 90d --ledger retention.jsonl --apply

Detection is heuristic. It will miss credentials it has no rule for. Read the
SCOPE section of README.txt before you rely on a redacted clip being clean.
`

func printHelp(w io.Writer) { fmt.Fprint(w, helpText) }

func usage() { printHelp(os.Stderr) }

// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------

func main() {
	args := os.Args[1:]
	if len(args) == 0 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// one question the program needs and stay on screen. Printing usage
		// and exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usage()
		os.Exit(1)
	}
	switch args[0] {
	case "help", "-h", "--help":
		printHelp(os.Stdout)
		os.Exit(0)
	case "version", "--version", "-V":
		fmt.Printf("%s %s\n", appName, appVersion)
		os.Exit(0)
	}
	cmd := args[0]
	rest := args[1:]
	for _, a := range rest {
		if a == "-h" || a == "--help" || a == "help" {
			printHelp(os.Stdout)
			os.Exit(0)
		}
	}
	switch cmd {
	case "index":
		cmdIndex(rest)
	case "list":
		cmdList(rest)
	case "search":
		cmdSearch(rest)
	case "scan":
		cmdScan(rest)
	case "redact":
		cmdRedact(rest)
	case "retain":
		cmdRetain(rest)
	default:
		usageErr("unknown command %q", cmd)
	}
}
