package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"text/tabwriter"

	"git.vakhrushev.me/av/healthlog/internal/config"
	"git.vakhrushev.me/av/healthlog/internal/store"
)

// uncoveredLimit — сколько строк перечня печатается по умолчанию.
//
// Предел объявлен, а не подразумевается: граница разбора в 32 имени действует на
// ОДНУ доставку, а различных имён журнал накопит сколько угодно — достаточно
// версии HAE, кладущей в ключ переменную часть. Двести взято с запасом: секций у
// HAE восемь, и перечень длиннее сотни означает не рост потока, а смену формы
// ключей — про неё скажет строка остатка.
const uncoveredLimit = 200

func runUncovered(args []string) error {
	fs := flag.NewFlagSet("uncovered", flag.ContinueOnError)
	cfgPath := fs.String("config", config.DefaultPath, "путь к config.toml")
	limit := fs.Int("limit", uncoveredLimit, "сколько строк перечня печатать")
	if err := fs.Parse(args); err != nil {
		if errors.Is(err, flag.ErrHelp) {
			// Справка — не отказ: иначе `uncovered -h` печатает usage и выходит
			// со словом «fatal» и кодом 1.
			return nil
		}
		return fmt.Errorf("parse flags: %w", err)
	}

	cfg, err := config.Load(*cfgPath)
	if err != nil {
		return err
	}

	// Только на чтение и без наката миграций: команда диагностическая, и запуск
	// её при живом сервисе не имеет права ни мигрировать схему, ни писать.
	// Расхождение версий — отказ с указанием обеих, и он доезжает до кода
	// возврата: молчаливый пустой перечень неотличим от «ничего не приезжало».
	st, err := store.OpenForRead(cfg.Storage.DBPath)
	if err != nil {
		return err
	}
	defer func() { _ = st.Close() }()

	sections, total, err := st.UncoveredSections(context.Background(), *limit)
	if err != nil {
		return err
	}

	writeUncovered(os.Stdout, sections, total)
	return nil
}

// writeUncovered печатает перечень человеку.
//
// Имя секции идёт ЭКРАНИРОВАННЫМ (`%q`): оно приходит верхнеуровневым ключом
// чужого тела, обрезано по длине на разборе, но по содержимому не ограничено
// ничем — сырая печать впустила бы в терминал управляющие последовательности.
//
// Данных о здоровье здесь нет: имя секции — структурный ключ, а не измерение.
// Идентификатор доставки печатается затем, чтобы по нему достать тело из архива
// и посмотреть форму секции глазами.
func writeUncovered(w io.Writer, sections []store.UncoveredSection, total int64) {
	if len(sections) == 0 {
		// НЕ «журнал такого не приносил»: перечень отвечает по колонкам
		// доживших учётных записей, а не по истории потока. Обещание, которое
		// носитель не даёт, закрыло бы владельцу вопрос ложным ответом.
		fmt.Fprintln(w, "В учётных записях журнала непокрытых секций сейчас нет.")
		writeUncoveredLimits(w)
		return
	}

	fmt.Fprintf(w, "Непокрытых секций: %d\n\n", total)

	tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
	fmt.Fprintln(tw, "СЕКЦИЯ\tДОСТАВОК\tПЕРВАЯ\tПОСЛЕДНЯЯ")
	for _, s := range sections {
		fmt.Fprintf(tw, "%q\t%d\t%s %s\t%s %s\n",
			s.Name, s.Deliveries,
			store.FormatTime(s.FirstSeen), s.FirstDeliveryID,
			store.FormatTime(s.LastSeen), s.LastDeliveryID)
	}
	_ = tw.Flush()

	// Остаток называется числом, а не обрывается молча: перечень — инструмент
	// диагностики, и «здесь всё» против «здесь двести из тысячи» это разные
	// ответы.
	if rest := total - int64(len(sections)); rest > 0 {
		fmt.Fprintf(w, "\nЕщё %d имён не показано.\n", rest)
	}
	writeUncoveredLimits(w)
}

// writeUncoveredLimits называет границы носителя — в любом исходе, включая
// пустой.
//
// Перечень производен от колонки учёта, а не от истории потока, и умолчать об
// этом значило бы отдать владельцу ответ, которого носитель не даёт: пустой
// перечень он прочитал бы как «ничего не приезжало» и закрыл бы вопрос.
func writeUncoveredLimits(w io.Writer) {
	fmt.Fprint(w, `
Перечень собран по колонке учёта `+"`delivery.uncovered_sections`"+`, и границ у неё три:
  - имена сверх 32 на одну доставку разбор в неё не кладёт;
  - пересборка заполняет колонку заново и только по сохранившимся телам;
  - секция, которую разбор научился покрывать, уходит из перечня при пересвёртке.
`)
}
