package main

import (
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"bufio"
	"github.com/pkg/errors"
	"github.com/projectatomic/libpod/libpod"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
)

type logOptions struct {
	details        bool
	follow         bool
	sinceTime      time.Time
	tail           uint64
	showTimestamps bool
}

var (
	logsFlags = []cli.Flag{
		cli.BoolFlag{
			Name:   "details",
			Usage:  "Show extra details provided to the logs",
			Hidden: true,
		},
		cli.BoolFlag{
			Name:  "follow, f",
			Usage: "Follow log output.  The default is false",
		},
		cli.StringFlag{
			Name:  "since",
			Usage: "Show logs since TIMESTAMP",
		},
		cli.Uint64Flag{
			Name:  "tail",
			Usage: "Output the specified number of LINES at the end of the logs.  Defaults to 0, which prints all lines",
		},
		cli.BoolFlag{
			Name:  "timestamps, t",
			Usage: "Output the timestamps in the log",
		},
		LatestFlag,
	}
	logsDescription = "The podman logs command batch-retrieves whatever logs are present for a container at the time of execution.  This does not guarantee execution" +
		"order when combined with podman run (i.e. your run may not have generated any logs at the time you execute podman logs"
	logsCommand = cli.Command{
		Name:           "logs",
		Usage:          "Fetch the logs of a container",
		Description:    logsDescription,
		Flags:          logsFlags,
		Action:         logsCmd,
		ArgsUsage:      "CONTAINER",
		SkipArgReorder: true,
	}
)

func logsCmd(c *cli.Context) error {
	var ctr *libpod.Container
	var err error
	if err := validateFlags(c, logsFlags); err != nil {
		return err
	}

	runtime, err := getRuntime(c)
	if err != nil {
		return errors.Wrapf(err, "could not get runtime")
	}
	defer runtime.Shutdown(false)

	args := c.Args()
	if len(args) != 1 && !c.Bool("latest") {
		return errors.Errorf("'podman logs' requires exactly one container name/ID")
	}

	sinceTime := time.Time{}
	if c.IsSet("since") {
		// parse time, error out if something is wrong
		since, err := parseInputTime(c.String("since"))
		if err != nil {
			return errors.Wrapf(err, "could not parse time: %q", c.String("since"))
		}
		sinceTime = since
	}

	opts := logOptions{
		details:        c.Bool("details"),
		follow:         c.Bool("follow"),
		sinceTime:      sinceTime,
		tail:           c.Uint64("tail"),
		showTimestamps: c.Bool("timestamps"),
	}

	if c.Bool("latest") {
		ctr, err = runtime.GetLatestContainer()
	} else {
		ctr, err = runtime.LookupContainer(args[0])
	}
	if err != nil {
		return err
	}

	logPath := ctr.LogPath()

	state, err := ctr.State()
	if err != nil {
		return err
	}

	// If the log file does not exist yet and the container is in the
	// Configured state, it has never been started before and no logs exist
	// Exit cleanly in this case
	if _, err := os.Stat(logPath); err != nil {
		if state == libpod.ContainerStateConfigured {
			logrus.Debugf("Container has not been started, no logs exist yet")
			return nil
		}
	}

	file, err := os.Open(logPath)
	if err != nil {
		return errors.Wrapf(err, "unable to read container log file")
	}
	defer file.Close()
	reader := bufio.NewReader(file)
	if opts.follow {
		followLog(reader, opts, ctr)
	} else {
		dumpLog(reader, opts)
	}
	return err
}

func followLog(reader *bufio.Reader, opts logOptions, ctr *libpod.Container) error {
	var cacheOutput []string
	firstPass := false
	if opts.tail > 0 {
		firstPass = true
	}
	// We need to read the entire file in here until we reach EOF
	// and then dump it out in the case that the user also wants
	// tail output
	for {
		line, err := reader.ReadString('\n')
		if err == io.EOF && opts.follow {
			if firstPass {
				firstPass = false
				cacheLen := int64(len(cacheOutput))
				start := int64(0)
				if cacheLen > int64(opts.tail) {
					start = cacheLen - int64(opts.tail)
				}
				for i := start; i < cacheLen; i++ {
					printLine(cacheOutput[i], opts)
				}
				continue
			}
			time.Sleep(1 * time.Second)
			// Check if container is still running or paused
			state, err := ctr.State()
			if err != nil {
				return err
			}
			if state != libpod.ContainerStateRunning && state != libpod.ContainerStatePaused {
				break
			}
			continue
		}
		// exits
		if err != nil {
			break
		}
		if firstPass {
			cacheOutput = append(cacheOutput, line)
			continue
		}
		printLine(line, opts)
	}
	return nil
}

func dumpLog(reader *bufio.Reader, opts logOptions) error {
	output := readLog(reader, opts)
	for _, line := range output {
		printLine(line, opts)
	}

	return nil
}

func readLog(reader *bufio.Reader, opts logOptions) []string {
	var output []string
	for {
		line, err := reader.ReadString('\n')
		if err != nil {
			break
		}
		output = append(output, line)
	}
	start := 0
	if opts.tail > 0 {
		if len(output) > int(opts.tail) {
			start = len(output) - int(opts.tail)
		}
	}
	return output[start:]
}

func printLine(line string, opts logOptions) {
	start := 3
	fields := strings.Fields(line)
	if opts.showTimestamps || !isStringTimestamp(fields[0]) {
		start = 0
	}
	if opts.sinceTime.IsZero() || logSinceTime(opts.sinceTime, fields[0]) {
		output := strings.Join(fields[start:], " ")
		fmt.Printf("%s\n", output)
	}
}

func isStringTimestamp(t string) bool {
	_, err := time.Parse("2006-01-02T15:04:05.999999999-07:00", t)
	if err != nil {
		return false
	}
	return true
}

// returns true if the time stamps of the logs are equal to or after the
// timestamp comparing to
func logSinceTime(sinceTime time.Time, logStr string) bool {
	timestamp := strings.Split(logStr, " ")[0]
	logTime, err := time.Parse("2006-01-02T15:04:05.999999999-07:00", timestamp)
	if err != nil {
		return false
	}
	return logTime.After(sinceTime) || logTime.Equal(sinceTime)
}

// secondSpaceIndex returns the index of the second space in a string
// In a line of the logs, the first two tokens are a timestamp and stdout/stderr,
// followed by the message itself.  This allows us to get the index of the message
// and avoid sending the other information back to the caller of GetLogs()
func secondSpaceIndex(line string) int {
	index := strings.Index(line, " ")
	if index == -1 {
		return 0
	}
	index = strings.Index(line[index:], " ")
	if index == -1 {
		return 0
	}
	return index
}

// parseInputTime takes the users input and to determine if it is valid and
// returns a time format and error.  The input is compared to known time formats
// or a duration which implies no-duration
func parseInputTime(inputTime string) (time.Time, error) {
	timeFormats := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05", "2006-01-02T15:04:05.999999999",
		"2006-01-02Z07:00", "2006-01-02"}
	// iterate the supported time formats
	for _, tf := range timeFormats {
		t, err := time.Parse(tf, inputTime)
		if err == nil {
			return t, nil
		}
	}

	// input might be a duration
	duration, err := time.ParseDuration(inputTime)
	if err != nil {
		return time.Time{}, errors.Errorf("unable to interpret time value")
	}
	return time.Now().Add(-duration), nil
}
