// goredo -- djb's redo implementation on pure Go
// Copyright (C) 2020-2026 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package main

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path"
	"sort"
	"strconv"
	"strings"
	"time"

	"go.stargrave.org/recfile/v4"
	"go.stargrave.org/tai64n/v5"
)

const HumanTimeFmt = "2006-01-02 15:04:05.000000000 Z07:00"

type BuildLogJob struct {
	tgt      *Tgt
	rec      map[string][]string
	started  time.Time
	exitCode int
}

type ByStarted []*BuildLogJob

func (a ByStarted) Len() int { return len(a) }

func (a ByStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

func (a ByStarted) Less(i, j int) bool {
	// actually that code performs reverse order
	if a[i].exitCode > a[j].exitCode {
		// bad return code has higher priority
		return true
	}
	return a[i].started.After(a[j].started)
}

var (
	flagBuildLogRecursive *bool
	flagBuildLogCommands  *bool
	buildLogSeen          map[string]struct{}
)

func init() {
	if CmdName() != CmdNameRedoLog {
		return
	}
	flagBuildLogRecursive = flag.Bool("r", false, "Show logs recursively")
	flagBuildLogCommands = flag.Bool("c", false, "Show how target was invoked")
	buildLogSeen = make(map[string]struct{})
}

func parseBuildLogRec(tgt *Tgt) (map[string][]string, error) {
	h, t := path.Split(tgt.a)
	fd, err := os.Open(path.Join(h, RedoDir, t+LogRecSuffix))
	if err != nil {
		return nil, ErrLine(err)
	}
	r := recfile.NewReader(bufio.NewReader(fd))
	rec, err := r.NextMapWithSlice()
	fd.Close()
	return rec, ErrLine(err)
}

func depthPrefix(depth int) string {
	if depth == 0 {
		return " "
	}
	return " " + colourize(CDebug, strings.Repeat("> ", depth))
}

func showBuildLogSub(sub *BuildLogJob, depth int) error {
	if _, ok := buildLogSeen[sub.tgt.rel]; ok {
		return nil
	}
	buildLogSeen[sub.tgt.rel] = struct{}{}
	dp := depthPrefix(depth)
	fmt.Printf(
		"%s%s%s\n",
		sub.rec["Started"][0], dp,
		colourize(CRedo, "redo "+sub.tgt.rel),
	)
	if err := showBuildLog(sub.tgt, sub.rec, depth+1); err != nil {
		return err
	}
	durationSec, durationNsec, err := durationToInts(sub.rec["Duration"][0])
	if err != nil {
		return ErrLine(err)
	}
	if sub.exitCode > 0 {
		fmt.Printf(
			"%s%s%s (code: %d) (%d.%ds)\n\n",
			sub.rec["Finished"][0], dp,
			colourize(CErr, "err "+sub.tgt.rel),
			sub.exitCode, durationSec, durationNsec,
		)
	} else {
		fmt.Printf(
			"%s%s%s (%d.%ds)\n\n",
			sub.rec["Finished"][0], dp,
			colourize(CRedo, "done "+sub.tgt.rel),
			durationSec, durationNsec,
		)
	}
	return nil
}

func durationToInts(d string) (int64, int64, error) {
	duration, err := strconv.ParseInt(d, 10, 64)
	if err != nil {
		return 0, 0, err
	}
	return duration / 1e9, (duration % 1e9) / 1000, nil
}

func showBuildLogCmd(m map[string][]string, depth int) error {
	started, err := tai64n.Decode(m["Started"][0])
	if err != nil {
		return ErrLine(err)
	}
	dp := depthPrefix(depth)
	fmt.Printf(
		"%s%s%s $ %s\n",
		m["Started"][0], dp, m["Cwd"][0], strings.Join(m["Cmd"], " "),
	)
	if len(m["ExitCode"]) > 0 {
		fmt.Printf("%s%sExit code: %s\n", m["Started"][0], dp, m["ExitCode"][0])
	}
	finished, err := tai64n.Decode(m["Finished"][0])
	if err != nil {
		return ErrLine(err)
	}
	durationSec, durationNsec, err := durationToInts(m["Duration"][0])
	if err != nil {
		return ErrLine(err)
	}
	fmt.Printf(
		"%s%sStarted:\t%s\n%s%sFinished:\t%s\n%s%sDuration:\t%d.%ds\n\n",
		m["Started"][0], dp, started.Format(HumanTimeFmt),
		m["Started"][0], dp, finished.Format(HumanTimeFmt),
		m["Started"][0], dp, durationSec, durationNsec,
	)
	return nil
}

func showBuildLog(tgt *Tgt, buildLogRec map[string][]string, depth int) error {
	var err error
	if *flagBuildLogCommands || *flagBuildLogRecursive {
		buildLogRec, err = parseBuildLogRec(tgt)
		if err != nil {
			return err
		}
	}
	if *flagBuildLogCommands {
		if err = showBuildLogCmd(buildLogRec, depth); err != nil {
			return err
		}
	}
	tgtH, tgtT := path.Split(tgt.a)
	fd, err := os.Open(path.Join(tgtH, RedoDir, tgtT+LogSuffix))
	if err != nil {
		return ErrLine(err)
	}
	if !*flagBuildLogRecursive {
		w := bufio.NewWriter(os.Stdout)
		_, err = io.Copy(w, bufio.NewReader(fd))
		fd.Close()
		if err != nil {
			w.Flush()
			return ErrLine(err)
		}
		return ErrLine(w.Flush())
	}
	defer fd.Close()
	subs := make([]*BuildLogJob, 0, len(buildLogRec["Ifchange"]))
	for _, depPath := range buildLogRec["Ifchange"] {
		dep := NewTgt(path.Join(tgtH, depPath))
		if dep.rel == tgt.rel {
			continue
		}
		var rec map[string][]string
		rec, err = parseBuildLogRec(dep)
		if err != nil {
			if errors.Is(err, fs.ErrNotExist) {
				continue
			}
			return err
		}
		if rec["Build"][0] != buildLogRec["Build"][0] {
			continue
		}
		var started time.Time
		started, err = tai64n.Decode(rec["Started"][0])
		if err != nil {
			return ErrLine(err)
		}
		var exitCode int
		if len(rec["ExitCode"]) > 0 {
			exitCode, err = strconv.Atoi(rec["ExitCode"][0])
			if err != nil {
				return ErrLine(err)
			}
		}
		subs = append(subs, &BuildLogJob{
			tgt:      dep,
			started:  started,
			exitCode: exitCode,
			rec:      rec,
		})
	}
	sort.Sort(ByStarted(subs))
	scanner := bufio.NewScanner(fd)
	var text string
	var sep int
	var t time.Time
	var sub *BuildLogJob
	if len(subs) > 0 {
		sub = subs[len(subs)-1]
	}
	dp := depthPrefix(depth)
	for {
		if !scanner.Scan() {
			if err = scanner.Err(); err != nil {
				return ErrLine(err)
			}
			break
		}
		text = scanner.Text()
		if text[0] != '@' {
			return ErrLine(errors.New("unexpected non-TAI64Ned string"))
		}
		sep = strings.IndexByte(text, byte(' '))
		if sep == -1 {
			sep = len(text)
		}
		t, err = tai64n.Decode(text[1:sep])
		if err != nil {
			return ErrLine(err)
		}
		for sub != nil && t.After(sub.started) {
			if err = showBuildLogSub(sub, depth); err != nil {
				return err
			}
			subs = subs[:len(subs)-1]
			if len(subs) > 0 {
				sub = subs[len(subs)-1]
			} else {
				sub = nil
			}
		}
		if depth == 0 {
			fmt.Println(text)
		} else {
			fmt.Printf("%s%s%s\n", text[:sep], dp, text[sep+1:])
		}
	}
	for i := len(subs); i > 0; i-- {
		sub = subs[i-1]
		if err = showBuildLogSub(sub, depth); err != nil {
			return err
		}
	}
	return nil
}
