package gitclient

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"strconv"
)

type pktKind int

const (
	pktData pktKind = iota
	pktFlush
	pktDelim
	pktResponseEnd
)

const maxPktLen = 65520

func writePkt(w io.Writer, line string) error {
	if len(line)+4 > maxPktLen {
		return fmt.Errorf("pkt-line too long: %d bytes", len(line))
	}
	_, err := fmt.Fprintf(w, "%04x%s", len(line)+4, line)
	return err
}

func writeFlush(w io.Writer) error {
	_, err := io.WriteString(w, "0000")
	return err
}

func writeDelim(w io.Writer) error {
	_, err := io.WriteString(w, "0001")
	return err
}

type pktReader struct {
	r   io.Reader
	buf []byte
}

func newPktReader(r io.Reader) *pktReader {
	return &pktReader{r: r, buf: make([]byte, maxPktLen)}
}

// payload is only valid until the next call
func (p *pktReader) next() (pktKind, []byte, error) {
	if _, err := io.ReadFull(p.r, p.buf[:4]); err != nil {
		if errors.Is(err, io.EOF) {
			err = io.ErrUnexpectedEOF
		}
		return 0, nil, err
	}
	n, err := strconv.ParseUint(string(p.buf[:4]), 16, 16)
	if err != nil {
		return 0, nil, fmt.Errorf("bad pkt-line length %q", p.buf[:4])
	}
	switch {
	case n == 0:
		return pktFlush, nil, nil
	case n == 1:
		return pktDelim, nil, nil
	case n == 2:
		return pktResponseEnd, nil, nil
	case n < 4 || n > maxPktLen:
		return 0, nil, fmt.Errorf("bad pkt-line length %d", n)
	}
	payload := p.buf[:n-4]
	if _, err := io.ReadFull(p.r, payload); err != nil {
		return 0, nil, err
	}
	return pktData, payload, nil
}

// skips acknowledgments/shallow-info; the reader yields band 1 of the
// packfile section, band 3 becomes an error
func packfileSection(p *pktReader) (io.Reader, error) {
	for {
		kind, line, err := p.next()
		if err != nil {
			return nil, err
		}
		if kind == pktFlush || kind == pktResponseEnd {
			return nil, errors.New("fetch response has no packfile section")
		}
		if msg, ok := bytes.CutPrefix(line, []byte("ERR ")); ok {
			return nil, fmt.Errorf("remote: %s", bytes.TrimSpace(msg))
		}
		if string(line) == "packfile\n" {
			return &sideband{p: p}, nil
		}
	}
}

type sideband struct {
	p    *pktReader
	data []byte
	done bool
}

func (s *sideband) Read(b []byte) (int, error) {
	for len(s.data) == 0 {
		if s.done {
			return 0, io.EOF
		}
		kind, line, err := s.p.next()
		if err != nil {
			return 0, err
		}
		if kind != pktData {
			s.done = true
			continue
		}
		if len(line) == 0 {
			return 0, errors.New("empty sideband pkt-line")
		}
		switch line[0] {
		case 1:
			s.data = line[1:]
		case 2:
		case 3:
			return 0, fmt.Errorf("remote: %s", bytes.TrimSpace(line[1:]))
		default:
			return 0, fmt.Errorf("bad sideband %d", line[0])
		}
	}
	n := copy(b, s.data)
	s.data = s.data[n:]
	return n, nil
}

// errors with ErrTooLarge once more than n bytes come through
type capReader struct {
	r io.Reader
	n int64
}

func (c *capReader) Read(b []byte) (int, error) {
	n, err := c.r.Read(b)
	c.n -= int64(n)
	if c.n < 0 {
		return 0, ErrTooLarge
	}
	return n, err
}
