package git

import (
	"fmt"
	"strings"

	gitcontract "bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/contracts/git"
	"bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/contracts/events"
	"bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/contracts/metadata"
)

// CommitLogFormat is the git log --format string used to capture full commit metadata.
const CommitLogFormat = "--format=%H%x00%P%x00%an%x00%ae%x00%aI%x00%cn%x00%ce%x00%cI%x00%B%x00"

const commitLogFieldCount = 9

// ParseNameStatus parses git diff --name-status output into file changes.
func ParseNameStatus(output string) ([]gitcontract.FileChange, error) {
	output = strings.TrimSpace(output)
	if output == "" {
		return nil, nil
	}

	var changes []gitcontract.FileChange
	for _, line := range strings.Split(output, "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		change, err := parseNameStatusLine(line)
		if err != nil {
			return nil, err
		}
		changes = append(changes, change)
	}
	return changes, nil
}

func parseNameStatusLine(line string) (gitcontract.FileChange, error) {
	parts := strings.Split(line, "\t")
	if len(parts) < 2 {
		return gitcontract.FileChange{}, fmt.Errorf("invalid name-status line: %q", line)
	}

	status := parts[0]
	path := parts[len(parts)-1]

	var changeType events.ChangeType
	var oldPath string

	switch {
	case strings.HasPrefix(status, "A"):
		changeType = events.ChangeTypeAdded
	case strings.HasPrefix(status, "M"):
		changeType = events.ChangeTypeModified
	case strings.HasPrefix(status, "D"):
		changeType = events.ChangeTypeDeleted
	case strings.HasPrefix(status, "R"):
		changeType = events.ChangeTypeRenamed
		if len(parts) >= 3 {
			oldPath = parts[1]
			path = parts[2]
		}
	default:
		return gitcontract.FileChange{}, fmt.Errorf("unsupported status %q in line: %q", status, line)
	}

	// git quotes paths containing special/non-ASCII bytes in double quotes with
	// C-style escapes (default core.quotepath). Decode them to real UTF-8 paths
	// so downstream classification and code-parser file reads work; otherwise a
	// path like "public/valentine\342\200\231s/x.png" is emitted verbatim (quotes
	// included), corrupting the extension/language and failing to open -> DLQ.
	path = unquoteGitPath(path)
	oldPath = unquoteGitPath(oldPath)

	language, fileKind := ClassifyFile(path)
	if changeType == events.ChangeTypeDeleted && language == "unknown" {
		language, fileKind = ClassifyFile(oldPath)
	}

	return gitcontract.FileChange{
		Path:       path,
		OldPath:    oldPath,
		ChangeType: changeType,
		Language:   language,
		FileKind:   fileKind,
	}, nil
}

// ParseCommitLog parses null-delimited git log output from CommitLogFormat.
func ParseCommitLog(output string) []gitcontract.CommitChange {
	output = strings.TrimSpace(output)
	if output == "" {
		return nil
	}

	parts := strings.Split(output, "\x00")
	var commits []gitcontract.CommitChange
	for i := 0; i < len(parts); {
		if strings.TrimSpace(parts[i]) == "" {
			i++
			continue
		}
		if i+commitLogFieldCount-1 >= len(parts) {
			break
		}

		parentField := strings.TrimSpace(parts[i+1])
		parent := ""
		if parentField != "" {
			parents := strings.Fields(parentField)
			if len(parents) > 0 {
				parent = parents[0]
			}
		}

		commits = append(commits, gitcontract.CommitChange{
			CommitSHA: strings.TrimSpace(parts[i]),
			ParentSHA: parent,
			GitCommitMetadata: metadata.GitCommitMetadata{
				AuthorName:     parts[i+2],
				AuthorEmail:    parts[i+3],
				AuthoredAt:     parts[i+4],
				CommitterName:  parts[i+5],
				CommitterEmail: parts[i+6],
				CommittedAt:    parts[i+7],
				CommitMessage:  strings.TrimSpace(parts[i+8]),
			},
		})
		i += commitLogFieldCount
	}
	return commits
}

// ClassifyAllFiles marks every tracked path as added (first sync).
func ClassifyAllFiles(paths []string) []gitcontract.FileChange {
	changes := make([]gitcontract.FileChange, 0, len(paths))
	for _, p := range paths {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		p = unquoteGitPath(p)
		language, fileKind := ClassifyFile(p)
		changes = append(changes, gitcontract.FileChange{
			Path:       p,
			ChangeType: events.ChangeTypeAdded,
			Language:   language,
			FileKind:   fileKind,
		})
	}
	return changes
}

// unquoteGitPath decodes a path as emitted by git when core.quotepath is on:
// paths containing special or non-ASCII bytes are wrapped in double quotes with
// C-style escapes (\t \n \" \\ and \NNN octal for raw bytes). Unquoted paths are
// returned unchanged. The decoded bytes form the real (UTF-8) path.
func unquoteGitPath(s string) string {
	if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
		return s
	}
	inner := s[1 : len(s)-1]
	var b []byte
	for i := 0; i < len(inner); i++ {
		c := inner[i]
		if c != '\\' {
			b = append(b, c)
			continue
		}
		i++
		if i >= len(inner) {
			b = append(b, '\\')
			break
		}
		switch e := inner[i]; e {
		case 'a':
			b = append(b, '\a')
		case 'b':
			b = append(b, '\b')
		case 't':
			b = append(b, '\t')
		case 'n':
			b = append(b, '\n')
		case 'v':
			b = append(b, '\v')
		case 'f':
			b = append(b, '\f')
		case 'r':
			b = append(b, '\r')
		case '"':
			b = append(b, '"')
		case '\\':
			b = append(b, '\\')
		default:
			if e >= '0' && e <= '7' {
				val := int(e - '0')
				for k := 0; k < 2 && i+1 < len(inner) && inner[i+1] >= '0' && inner[i+1] <= '7'; k++ {
					i++
					val = val*8 + int(inner[i]-'0')
				}
				b = append(b, byte(val))
			} else {
				b = append(b, e)
			}
		}
	}
	return string(b)
}
