// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package attribute

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"code.gitea.io/gitea/modules/git"
	"code.gitea.io/gitea/modules/git/gitcmd"
	"code.gitea.io/gitea/modules/log"
)

// BatchChecker provides a reader for check-attribute content that can be long running
type BatchChecker struct {
	attributesNum int
	repo          *git.Repository
	stdinWriter   *os.File
	stdOut        *nulSeparatedAttributeWriter
	ctx           context.Context
	cancel        context.CancelFunc
	cmd           *gitcmd.Command
}

// NewBatchChecker creates a check attribute reader for the current repository and provided commit ID
// If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo
func NewBatchChecker(repo *git.Repository, treeish string, attributes []string) (checker *BatchChecker, returnedErr error) {
	ctx, cancel := context.WithCancel(repo.Ctx)
	defer func() {
		if returnedErr != nil {
			cancel()
		}
	}()

	cmd, envs, cleanup, err := checkAttrCommand(repo, treeish, nil, attributes)
	if err != nil {
		return nil, err
	}
	defer func() {
		if returnedErr != nil {
			cleanup()
		}
	}()

	cmd.AddArguments("--stdin")

	checker = &BatchChecker{
		attributesNum: len(attributes),
		repo:          repo,
		ctx:           ctx,
		cmd:           cmd,
		cancel: func() {
			cancel()
			cleanup()
		},
	}

	stdinReader, stdinWriter, err := os.Pipe()
	if err != nil {
		return nil, err
	}
	checker.stdinWriter = stdinWriter

	lw := new(nulSeparatedAttributeWriter)
	lw.attributes = make(chan attributeTriple, len(attributes))
	lw.closed = make(chan struct{})
	checker.stdOut = lw

	go func() {
		defer func() {
			_ = stdinReader.Close()
			_ = lw.Close()
		}()
		stdErr := new(bytes.Buffer)
		err := cmd.Run(ctx, &gitcmd.RunOpts{
			Env:    envs,
			Dir:    repo.Path,
			Stdin:  stdinReader,
			Stdout: lw,
			Stderr: stdErr,
		})

		if err != nil && !git.IsErrCanceledOrKilled(err) {
			log.Error("Attribute checker for commit %s exits with error: %v", treeish, err)
		}
		checker.cancel()
	}()

	return checker, nil
}

// CheckPath check attr for given path
func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) {
	defer func() {
		if err != nil && err != c.ctx.Err() {
			log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.repo.Path), err)
		}
	}()

	select {
	case <-c.ctx.Done():
		return nil, c.ctx.Err()
	default:
	}

	if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil {
		defer c.Close()
		return nil, err
	}

	reportTimeout := func() error {
		stdOutClosed := false
		select {
		case <-c.stdOut.closed:
			stdOutClosed = true
		default:
		}
		debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.repo.Path))
		debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed)
		if c.cmd != nil {
			debugMsg += fmt.Sprintf(", process state: %q", c.cmd.ProcessState())
		}
		_ = c.Close()
		return fmt.Errorf("CheckPath timeout: %s", debugMsg)
	}

	rs = NewAttributes()
	for i := 0; i < c.attributesNum; i++ {
		select {
		case <-time.After(5 * time.Second):
			// there is no "hang" problem now. This code is just used to catch other potential problems.
			return nil, reportTimeout()
		case attr, ok := <-c.stdOut.ReadAttribute():
			if !ok {
				return nil, c.ctx.Err()
			}
			rs.m[attr.Attribute] = Attribute(attr.Value)
		case <-c.ctx.Done():
			return nil, c.ctx.Err()
		}
	}
	return rs, nil
}

func (c *BatchChecker) Close() error {
	c.cancel()
	err := c.stdinWriter.Close()
	return err
}

type attributeTriple struct {
	Filename  string
	Attribute string
	Value     string
}

type nulSeparatedAttributeWriter struct {
	tmp        []byte
	attributes chan attributeTriple
	closed     chan struct{}
	working    attributeTriple
	pos        int
}

func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
	l, read := len(p), 0

	nulIdx := bytes.IndexByte(p, '\x00')
	for nulIdx >= 0 {
		wr.tmp = append(wr.tmp, p[:nulIdx]...)
		switch wr.pos {
		case 0:
			wr.working = attributeTriple{
				Filename: string(wr.tmp),
			}
		case 1:
			wr.working.Attribute = string(wr.tmp)
		case 2:
			wr.working.Value = string(wr.tmp)
		}
		wr.tmp = wr.tmp[:0]
		wr.pos++
		if wr.pos > 2 {
			wr.attributes <- wr.working
			wr.pos = 0
		}
		read += nulIdx + 1
		if l > read {
			p = p[nulIdx+1:]
			nulIdx = bytes.IndexByte(p, '\x00')
		} else {
			return l, nil
		}
	}
	wr.tmp = append(wr.tmp, p...)
	return l, nil
}

func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
	return wr.attributes
}

func (wr *nulSeparatedAttributeWriter) Close() error {
	select {
	case <-wr.closed:
		return nil
	default:
	}
	close(wr.attributes)
	close(wr.closed)
	return nil
}
