// verify.go — o verificador aberto do TUBE. Quem grava produz um PACOTE com o
// codificador (fechado); quem audita roda este programa usando SÓ o
// decodificador, sem confiar em quem gravou.
//
//	tube-verifica <prefixo.manifest.json> [originais.csv[.gz]] [coluna]
//
// O pacote são três arquivos sidecar: prefix.v4s (stream), prefix.idx
// (índice min/max por GOP, codec4x) e prefix.manifest.json (n, bound,
// keyframe + SHA-256 de stream e índice).
//
// O -verify prova três coisas, em ordem de custo:
//  1. INTEGRIDADE — os SHA-256 do manifest batem com os arquivos;
//  2. ÍNDICE HONESTO — reconstrói o índice a partir da DECODIFICAÇÃO e
//     compara byte a byte com o embarcado: toda poda de query que o índice
//     autorizar está coberta pela reconstrução real;
//  3. BOUND CUMPRIDO (com os originais em mãos) — |orig − rec| ≤ ε para
//     TODA amostra, com máximo e argmax reportados.
//
// (1)+(2) auditam um arquivo frio sem os originais; (3) é a prova completa
// de um piloto. Saída: veredito por prova e exit code 0/1 — scriptável.
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math"
	"os"
	"path/filepath"
	"strconv"
)

type bundleManifest struct {
	Name     string  `json:"name"`
	N        int     `json:"n"`
	Bound    float64 `json:"bound"`
	Keyframe int     `json:"keyframe"`
	Stream   string  `json:"stream"`
	Index    string  `json:"index"`
	StreamSH string  `json:"stream_sha256"`
	IndexSH  string  `json:"index_sha256"`
}

func sha256hex(b []byte) string {
	h := sha256.Sum256(b)
	return hex.EncodeToString(h[:])
}

// loadAutoColumn tenta colunas 0..3 até achar floats (cabeçalhos são pulados
// pelo loadRealColumn); col >= 0 força a coluna.
func loadAutoColumn(path string, col int) ([]float64, error) {
	if col >= 0 {
		return loadRealColumn(path, col)
	}
	var data []float64
	var err error
	for c := 0; c < 4 && len(data) == 0; c++ {
		data, err = loadRealColumn(path, c)
	}
	return data, err
}

func runVerify(args []string) {
	if len(args) < 1 {
		fmt.Println("uso: tube-verifica <prefixo.manifest.json> [originais.csv[.gz]] [coluna]")
		os.Exit(2)
	}
	mj, err := os.ReadFile(args[0])
	if err != nil {
		fmt.Println("erro lendo manifest:", err)
		os.Exit(1)
	}
	var m bundleManifest
	if err := json.Unmarshal(mj, &m); err != nil {
		fmt.Println("manifest inválido:", err)
		os.Exit(1)
	}
	// os arquivos do pacote moram ao lado do manifest, só pelo nome: um
	// manifest não pode apontar para fora do próprio diretório
	dir := filepath.Dir(args[0])
	if filepath.Base(m.Stream) != m.Stream || filepath.Base(m.Index) != m.Index {
		fmt.Println("manifest inválido: stream e index devem ser nomes simples, sem diretório")
		os.Exit(1)
	}
	enc, err1 := os.ReadFile(filepath.Join(dir, m.Stream))
	idx, err2 := os.ReadFile(filepath.Join(dir, m.Index))
	if err1 != nil || err2 != nil {
		fmt.Println("erro lendo pacote:", err1, err2)
		os.Exit(1)
	}
	fail, semOriginais := false, false
	check := func(nome string, okB bool, detalhe string) {
		fmt.Printf("%-16s %s  %s\n", nome, ok(okB), detalhe)
		if !okB {
			fail = true
		}
	}

	// 1. integridade
	check("integridade", sha256hex(enc) == m.StreamSH && sha256hex(idx) == m.IndexSH,
		fmt.Sprintf("sha256 de %s e %s contra o manifest", m.Stream, m.Index))

	// 2. índice honesto: decodifica e reconstrói o índice do zero.
	// Bytes forjados não podem travar nem derrubar o verificador: a
	// decodificação roda sob limite e qualquer falha vira reprovação.
	rec, motivo := decodifica(enc, m.N, m.Keyframe, m.Bound)
	if motivo != "" {
		check("decodificação", false, "fluxo malformado: "+motivo)
		fmt.Println("\nVEREDITO: FALHOU")
		os.Exit(1)
	}
	rebuilt := buildV4SeekIndexFromRec(rec, m.Keyframe, m.Bound).encode()
	check("índice honesto", string(rebuilt) == string(idx),
		fmt.Sprintf("%d GOPs reconstruídos da decodificação = embarcado, byte a byte",
			(m.N+m.Keyframe-1)/m.Keyframe))

	// 3. bound cumprido, se os originais vieram
	if len(args) > 1 {
		col := -1
		if len(args) > 2 {
			col, _ = strconv.Atoi(args[2])
		}
		orig, err := loadAutoColumn(args[1], col)
		if err != nil || len(orig) == 0 {
			fmt.Println("erro lendo originais:", err)
			os.Exit(1)
		}
		if len(orig) != m.N {
			check("bound cumprido", false,
				fmt.Sprintf("n difere: originais %d, manifest %d", len(orig), m.N))
		} else {
			maxErr, argmax := 0.0, 0
			for i, v := range orig {
				if e := math.Abs(v - rec[i]); e > maxErr {
					maxErr, argmax = e, i
				}
			}
			// contrato do codec: erro ≤ bound·(1+1e-9). A folga de 1e-9 é a de
			// arredondamento de ponto flutuante do quantizador, a mesma que os
			// testes do codificador usam
			cumpre := maxErr <= m.Bound*(1+1e-9)
			op := "≤"
			if !cumpre {
				op = ">"
			}
			check("bound cumprido", cumpre,
				fmt.Sprintf("max|orig−rec| = %.6g %s ε=%g·(1+1e-9) (amostra %d), %d amostras",
					maxErr, op, m.Bound, argmax, m.N))
		}
	} else {
		semOriginais = true
		fmt.Printf("%-16s —  sem originais: provas 1–2 auditam o arquivo frio\n", "bound cumprido")
	}

	if fail {
		fmt.Println("\nVEREDITO: FALHOU")
		os.Exit(1)
	}
	if semOriginais {
		// medido: com o manifest reescrito, 4,1% das trocas de 1 bit mudam a
		// série e mantêm o índice; só os originais ou a âncora no razão pegam
		fmt.Println("\nVEREDITO: VERIFICADO SÓ COERÊNCIA — o pacote bate com o próprio manifest.")
		fmt.Println("Sem os originais, confira que o manifest está selado no razão:")
		fmt.Println("  python3 girder/verifica.py PASTA_DO_RAZAO --chave HEX --manifesto", args[0])
		return
	}
	fmt.Println("\nVEREDITO: VERIFICADO")
}

// decodifica devolve a reconstrução ou o motivo pelo qual os bytes não são um
// fluxo TUBE v4 válido.
func decodifica(enc []byte, n, keyframe int, bound float64) (rec []float64, motivo string) {
	// cada GOP custa ao menos 1 byte (o tamanho do bloco), então um fluxo de L
	// bytes não descreve mais que L·keyframe amostras; o teto absoluto segura a
	// memória (8 bytes por amostra) diante de um manifest forjado
	if keyframe <= 0 || keyframe > 1<<20 || n < 0 || n > 1<<27 || n > len(enc)*keyframe {
		return nil, fmt.Sprintf("manifest incompatível com o fluxo (n=%d, keyframe=%d, %d bytes)", n, keyframe, len(enc))
	}
	defer func() {
		if r := recover(); r != nil {
			rec, motivo = nil, fmt.Sprint(r)
		}
	}()
	rec = decodeChannelV4Seek(enc, n, keyframe, bound, 0)
	if len(rec) != n {
		return nil, "decodificou quantidade errada de amostras"
	}
	return rec, ""
}
