package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"slices"
	"strconv"
	"strings"
	"text/tabwriter"
	"time"

	"hakurei.app/hst"
	"hakurei.app/internal/outcome"
	"hakurei.app/internal/store"
	"hakurei.app/message"
)

// printShowSystem populates and writes a representation of [hst.Info] to output.
func printShowSystem(output io.Writer, short, flagJSON bool) {
	t := newPrinter(output)
	defer t.MustFlush()
	hi := outcome.Info()

	if flagJSON {
		encodeJSON(log.Fatal, output, short, hi)
		return
	}

	t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
	t.Printf("User:\t%d\n", hi.User)
	t.Printf("TempDir:\t%s\n", hi.TempDir)
	t.Printf("SharePath:\t%s\n", hi.SharePath)
	t.Printf("RuntimePath:\t%s\n", hi.RuntimePath)
	t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
}

// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
func printShowInstance(
	output io.Writer, now time.Time,
	instance *hst.State, config *hst.Config,
	short, flagJSON bool,
) (valid bool) {
	valid = true

	if flagJSON {
		if instance != nil {
			encodeJSON(log.Fatal, output, short, instance)
		} else {
			encodeJSON(log.Fatal, output, short, config)
		}
		return
	}

	t := newPrinter(output)
	defer t.MustFlush()

	if err := config.Validate(hst.VAllowInsecure); err != nil {
		valid = false
		if m, ok := message.GetMessage(err); ok {
			mustPrint(output, "Error: "+m+"!\n\n")
		}
	}

	if config == nil {
		// nothing to print
		return
	}

	if instance != nil {
		t.Printf("State\n")
		t.Printf(" Instance:\t%s (%d -> %d)\n", instance.ID.String(), instance.PID, instance.ShimPID)
		t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
		t.Printf("\n")
	}

	t.Printf("App\n")
	if config.ID != "" {
		t.Printf(" Identity:\t%d (%s)\n", config.Identity, config.ID)
	} else {
		t.Printf(" Identity:\t%d\n", config.Identity)
	}
	t.Printf(" Enablements:\t%s\n", config.Enablements.Unwrap().String())
	if len(config.Groups) > 0 {
		t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
	}
	if config.Container != nil {
		flags := config.Container.Flags.String()

		// this is included in the upper hst.Config struct but is relevant here
		const flagDirectWayland = "directwl"
		if config.DirectWayland {
			// hardcoded value when every flag is unset
			if flags == "none" {
				flags = flagDirectWayland
			} else {
				flags += ", " + flagDirectWayland
			}
		}
		t.Printf(" Flags:\t%s\n", flags)

		if config.Container.Home != nil {
			t.Printf(" Home:\t%s\n", config.Container.Home)
		}
		if config.Container.Hostname != "" {
			t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
		}
		if config.Container.Path != nil {
			t.Printf(" Path:\t%s\n", config.Container.Path)
		}
		if len(config.Container.Args) > 0 {
			t.Printf(" Arguments:\t%s\n", strings.Join(config.Container.Args, " "))
		}
	}
	t.Printf("\n")

	if !short {
		if config.Container != nil && len(config.Container.Filesystem) > 0 {
			t.Printf("Filesystem\n")
			for _, f := range config.Container.Filesystem {
				if !f.Valid() {
					valid = false
					t.Println(" <invalid>")
					continue
				}
				t.Printf(" %s\n", f)
			}
			t.Printf("\n")
		}
		if len(config.ExtraPerms) > 0 {
			t.Printf("Extra ACL\n")
			for i := range config.ExtraPerms {
				t.Printf(" %s\n", config.ExtraPerms[i].String())
			}
			t.Printf("\n")
		}
	}

	printDBus := func(c *hst.BusConfig) {
		t.Printf(" Filter:\t%v\n", c.Filter)
		if len(c.See) > 0 {
			t.Printf(" See:\t%q\n", c.See)
		}
		if len(c.Talk) > 0 {
			t.Printf(" Talk:\t%q\n", c.Talk)
		}
		if len(c.Own) > 0 {
			t.Printf(" Own:\t%q\n", c.Own)
		}
		if len(c.Call) > 0 {
			t.Printf(" Call:\t%q\n", c.Call)
		}
		if len(c.Broadcast) > 0 {
			t.Printf(" Broadcast:\t%q\n", c.Broadcast)
		}
	}
	if config.SessionBus != nil {
		t.Printf("Session bus\n")
		printDBus(config.SessionBus)
		t.Printf("\n")
	}
	if config.SystemBus != nil {
		t.Printf("System bus\n")
		printDBus(config.SystemBus)
		t.Printf("\n")
	}

	return
}

// printPs writes a representation of active instances to output.
func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, short, flagJSON bool) {
	f := func(a func(eh *store.EntryHandle)) {
		entries, copyError := s.All()
		for eh := range entries {
			a(eh)
		}
		if err := copyError(); err != nil {
			msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
		}
	}

	if short { // short output requires identifier only
		var identifiers []*hst.ID
		f(func(eh *store.EntryHandle) {
			if _, err := eh.Load(nil); err != nil { // passes through decode error
				msg.GetLogger().Println(getMessage("cannot validate state entry header:", err))
				return
			}
			identifiers = append(identifiers, &eh.ID)
		})
		slices.SortFunc(identifiers, func(a, b *hst.ID) int { return bytes.Compare(a[:], b[:]) })

		if flagJSON {
			encodeJSON(log.Fatal, output, short, identifiers)
		} else {
			for _, id := range identifiers {
				mustPrintln(output, shortIdentifier(id))
			}
		}
		return
	}

	// long output requires full instance state
	var instances []*hst.State
	f(func(eh *store.EntryHandle) {
		var state hst.State
		if _, err := eh.Load(&state); err != nil { // passes through decode error
			msg.GetLogger().Println(getMessage("cannot load state entry:", err))
			return
		}
		instances = append(instances, &state)
	})
	slices.SortFunc(instances, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) })

	if flagJSON {
		encodeJSON(log.Fatal, output, short, instances)
		return
	}

	t := newPrinter(output)
	defer t.MustFlush()

	t.Println("\tInstance\tPID\tApplication\tUptime")
	for _, instance := range instances {
		as := "(No configuration information)"
		if instance.Config != nil {
			as = strconv.Itoa(instance.Config.Identity)
			id := instance.Config.ID
			if id == "" {
				id = "app.hakurei." + shortIdentifier(&instance.ID)
			}
			as += " (" + id + ")"
		}
		t.Printf("\t%s\t%d\t%s\t%s\n",
			shortIdentifier(&instance.ID), instance.PID, as, now.Sub(instance.Time).Round(time.Second).String())
	}
}

// newPrinter returns a configured, wrapped [tabwriter.Writer].
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }

// tp wraps [tabwriter.Writer] to provide additional formatting methods.
type tp struct{ *tabwriter.Writer }

// Printf calls [fmt.Fprintf] on the underlying [tabwriter.Writer].
func (p *tp) Printf(format string, a ...any) {
	if _, err := fmt.Fprintf(p, format, a...); err != nil {
		log.Fatalf("cannot write to tabwriter: %v", err)
	}
}

// Println calls [fmt.Fprintln] on the underlying [tabwriter.Writer].
func (p *tp) Println(a ...any) {
	if _, err := fmt.Fprintln(p, a...); err != nil {
		log.Fatalf("cannot write to tabwriter: %v", err)
	}
}

// MustFlush calls the Flush method of [tabwriter.Writer] and calls [log.Fatalf] on a non-nil error.
func (p *tp) MustFlush() {
	if err := p.Writer.Flush(); err != nil {
		log.Fatalf("cannot flush tabwriter: %v", err)
	}
}

func mustPrint(output io.Writer, a ...any) {
	if _, err := fmt.Fprint(output, a...); err != nil {
		log.Fatalf("cannot print: %v", err)
	}
}
func mustPrintln(output io.Writer, a ...any) {
	if _, err := fmt.Fprintln(output, a...); err != nil {
		log.Fatalf("cannot print: %v", err)
	}
}

// getMessage returns a [message.Error] message if available, or err prefixed with fallback otherwise.
func getMessage(fallback string, err error) string {
	if m, ok := message.GetMessage(err); ok {
		return m
	}
	return fmt.Sprintln(fallback, err)
}
