package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"net/url"
	"os"
	"os/signal"
	"strings"
	"time"
)

func runLogs(args []string) error {
	fs := flag.NewFlagSet("logs", flag.ExitOnError)
	g := addGlobalFlags(fs)
	follow := fs.Bool("f", false, "follow the log stream (Ctrl-C to stop)")
	tail := fs.Int("tail", 200, "number of trailing lines to show (max 5000)")
	container := fs.String("container", "", "container row id/prefix or role (when an app has several)")
	fs.Usage = func() {
		fmt.Fprint(os.Stderr, "Usage: tinyforge logs <app> [-f] [--tail N] [--container CID]\n\nPrint or follow a container's logs.\n")
		fs.PrintDefaults()
	}
	if err := fs.Parse(args); err != nil {
		return err
	}
	if fs.NArg() != 1 {
		fs.Usage()
		return fmt.Errorf("expected exactly one app (name or id)")
	}

	sess, err := newSession(g)
	if err != nil {
		return err
	}

	resolveCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	app, err := resolveApp(resolveCtx, sess.client, fs.Arg(0))
	if err != nil {
		return err
	}

	var containers []Container
	if err := sess.client.doJSON(resolveCtx, "GET", "/api/workloads/"+app.ID+"/containers", nil, &containers); err != nil {
		return err
	}
	target, err := chooseContainer(containers, *container)
	if err != nil {
		return err
	}

	q := url.Values{}
	q.Set("tail", fmt.Sprintf("%d", *tail))
	base := "/api/workloads/" + app.ID + "/containers/" + target.ID + "/logs"

	if !*follow {
		var lines []string
		if err := sess.client.doJSON(resolveCtx, "GET", base+"?"+q.Encode(), nil, &lines); err != nil {
			return err
		}
		for _, line := range lines {
			fmt.Println(line)
		}
		return nil
	}

	// Follow: stream until EOF or Ctrl-C.
	q.Set("follow", "true")
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	err = sess.client.streamSSE(ctx, base+"?"+q.Encode(), func(payload []byte) error {
		var frame struct {
			Line string `json:"line"`
		}
		if json.Unmarshal(payload, &frame) != nil {
			return nil // ignore frames we can't parse
		}
		fmt.Println(frame.Line)
		return nil
	})
	if ctx.Err() != nil { // user interrupted — clean exit
		return nil
	}
	return err
}

// chooseContainer selects which container to read. With an explicit selector,
// it matches the row id (exact or prefix) or the role. Otherwise it uses the
// sole container, or the sole running one, and errors with a list when the
// choice is ambiguous.
func chooseContainer(cs []Container, selector string) (Container, error) {
	if len(cs) == 0 {
		return Container{}, fmt.Errorf("app has no containers yet — deploy it first")
	}

	if selector != "" {
		var matches []Container
		for _, c := range cs {
			if c.ID == selector || strings.EqualFold(c.Role, selector) ||
				(len(selector) >= 6 && strings.HasPrefix(c.ID, selector)) {
				matches = append(matches, c)
			}
		}
		switch len(matches) {
		case 1:
			return matches[0], nil
		case 0:
			return Container{}, fmt.Errorf("no container matching %q\n%s", selector, containerList(cs))
		default:
			return Container{}, fmt.Errorf("%q matches multiple containers\n%s", selector, containerList(cs))
		}
	}

	if len(cs) == 1 {
		return cs[0], nil
	}
	var running []Container
	for _, c := range cs {
		if c.State == "running" {
			running = append(running, c)
		}
	}
	if len(running) == 1 {
		return running[0], nil
	}
	return Container{}, fmt.Errorf("app has %d containers; pick one with --container:\n%s", len(cs), containerList(cs))
}

func containerList(cs []Container) string {
	var b strings.Builder
	for _, c := range cs {
		role := c.Role
		if role == "" {
			role = "(default)"
		}
		fmt.Fprintf(&b, "  %s  %-12s %s\n", idShort(c.ID), role, c.State)
	}
	return strings.TrimRight(b.String(), "\n")
}
