// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0

package gitinterface

import (
	"errors"
	"fmt"
	"os"
	"strings"
)

// See https://git-scm.com/docs/git-status#_porcelain_format_version_1.

var (
	ErrInvalidStatusCodeLength = errors.New("status code string must be of length 1")
	ErrInvalidStatusCode       = errors.New("status code string is unrecognized")
)

type StatusCode uint

const (
	StatusCodeUnmodified StatusCode = iota + 1 // we use 0 as error code
	StatusCodeModified
	StatusCodeTypeChanged
	StatusCodeAdded
	StatusCodeDeleted
	StatusCodeRenamed
	StatusCodeCopied
	StatusCodeUpdatedUnmerged
	StatusCodeUntracked
	StatusCodeIgnored
)

func (s StatusCode) String() string {
	switch s {
	case StatusCodeUnmodified:
		return " " // is this actually a space or empty string?
	case StatusCodeModified:
		return "M"
	case StatusCodeTypeChanged:
		return "T"
	case StatusCodeAdded:
		return "A"
	case StatusCodeDeleted:
		return "D"
	case StatusCodeRenamed:
		return "R"
	case StatusCodeCopied:
		return "C"
	case StatusCodeUpdatedUnmerged:
		return "U"
	case StatusCodeUntracked:
		return "?"
	case StatusCodeIgnored:
		return "!"
	default:
		return "invalid-code"
	}
}

func NewStatusCodeFromByte(s byte) (StatusCode, error) {
	switch s {
	case ' ':
		return StatusCodeUnmodified, nil
	case 'M':
		return StatusCodeModified, nil
	case 'T':
		return StatusCodeTypeChanged, nil
	case 'A':
		return StatusCodeAdded, nil
	case 'D':
		return StatusCodeDeleted, nil
	case 'C':
		return StatusCodeCopied, nil
	case 'U':
		return StatusCodeUpdatedUnmerged, nil
	case '?':
		return StatusCodeUntracked, nil
	case '!':
		return StatusCodeIgnored, nil
	default:
		return 0, ErrInvalidStatusCode
	}
}

type FileStatus struct {
	X StatusCode
	Y StatusCode
}

func (f *FileStatus) Untracked() bool {
	return f.X == StatusCodeUntracked || f.Y == StatusCodeUntracked
}

func (r *Repository) Status() (map[string]FileStatus, error) {
	worktree := r.gitDirPath
	if !r.IsBare() {
		worktree = strings.TrimSuffix(worktree, ".git") // TODO: this doesn't support detached git dir
	}
	cwd, err := os.Getwd()
	if err != nil {
		return nil, err
	}
	if err := os.Chdir(worktree); err != nil {
		return nil, err
	}
	defer os.Chdir(cwd) //nolint:errcheck

	output, err := r.executor("status", "--porcelain=1", "-z", "--untracked-files=all", "--ignored").executeString()
	if err != nil {
		return nil, fmt.Errorf("unable to check status of repository: %w", err)
	}

	statuses := map[string]FileStatus{}

	lines := strings.Split(output, string('\000'))
	for _, line := range lines {
		if len(line) == 0 {
			continue
		}

		// first two characters are status codes, find the corresponding
		// statuses
		xb := line[0]
		yb := line[1]
		// Note: we identify the status after inspecting the path so we can
		// provide better error messages

		// then, we have a single space followed by the path, ignore space and
		// read in the rest as the filepath
		filePath := strings.TrimSpace(line[2:])

		xStatus, err := NewStatusCodeFromByte(xb)
		if err != nil {
			return nil, fmt.Errorf("unable to parse status code '%c' for path '%s': %w", xb, filePath, err)
		}

		yStatus, err := NewStatusCodeFromByte(yb)
		if err != nil {
			return nil, fmt.Errorf("unable to parse status code '%c' for path '%s': %w", yb, filePath, err)
		}

		status := FileStatus{X: xStatus, Y: yStatus}

		statuses[filePath] = status
	}

	return statuses, nil
}
