package parser

import (
	"errors"
	"fmt"
	"os/exec"
	"path/filepath"
	"strings"

	contractevents "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/events"
)

// ErrNoCodeFilesChanged indicates a commit diff has no indexable code file changes.
var ErrNoCodeFilesChanged = errors.New("no code files changed")

// GitEmptyTreeSHA is the canonical empty tree object used for root-commit diffs.
const GitEmptyTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"

func isRootCommitBase(baseSHA string) bool {
	baseSHA = strings.TrimSpace(baseSHA)
	return baseSHA == "" || baseSHA == GitEmptyTreeSHA
}

// CommitDiff holds resolved technical commit diff metadata for graph comparison.
type CommitDiff struct {
	RepoID       string
	SnapshotID   string
	BaseSHA      string
	TargetSHA    string
	ChangedFiles []string
}

var gitChangedFiles = defaultGitChangedFiles

// ResolveCommitDiff resolves base/target SHAs and changed code files for a commits.changed event.
func ResolveCommitDiff(workspacePath string, event contractevents.CommitsChangedEvent) (CommitDiff, error) {
	targetSHA := strings.TrimSpace(event.CommitSHA)
	if targetSHA == "" {
		return CommitDiff{}, fmt.Errorf("commit_sha is required")
	}

	baseSHA := strings.TrimSpace(event.ParentSHA)

	normalized := normalizeChangedFiles(event.ChangedFiles)
	changedFiles := filterCodeFiles(normalized)
	if len(changedFiles) == 0 && len(normalized) > 0 {
		if baseSHA == "" {
			return CommitDiff{}, fmt.Errorf("no code files in root commit %s", targetSHA)
		}
		return CommitDiff{}, fmt.Errorf("%w between %s and %s", ErrNoCodeFilesChanged, baseSHA, targetSHA)
	}
	if len(changedFiles) == 0 {
		gitBaseSHA := baseSHA
		if isRootCommitBase(gitBaseSHA) {
			gitBaseSHA = GitEmptyTreeSHA
		}
		files, err := gitChangedFiles(workspacePath, event.RepoID, gitBaseSHA, targetSHA)
		if err != nil {
			return CommitDiff{}, fmt.Errorf("resolve changed files via git: %w", err)
		}
		changedFiles = filterCodeFiles(files)
	}

	if len(changedFiles) == 0 {
		if baseSHA == "" {
			return CommitDiff{}, fmt.Errorf("no code files in root commit %s", targetSHA)
		}
		return CommitDiff{}, fmt.Errorf("%w between %s and %s", ErrNoCodeFilesChanged, baseSHA, targetSHA)
	}

	outputBaseSHA := baseSHA
	if isRootCommitBase(outputBaseSHA) {
		outputBaseSHA = GitEmptyTreeSHA
	}

	return CommitDiff{
		RepoID:       event.RepoID,
		SnapshotID:   event.SnapshotID,
		BaseSHA:      outputBaseSHA,
		TargetSHA:    targetSHA,
		ChangedFiles: changedFiles,
	}, nil
}

// ResolveNonCodeCommitDiff resolves all changed files for commits without code deltas.
func ResolveNonCodeCommitDiff(workspacePath string, event contractevents.CommitsChangedEvent) (CommitDiff, error) {
	targetSHA := strings.TrimSpace(event.CommitSHA)
	if targetSHA == "" {
		return CommitDiff{}, fmt.Errorf("commit_sha is required")
	}

	baseSHA := strings.TrimSpace(event.ParentSHA)
	normalized := normalizeChangedFiles(event.ChangedFiles)
	allFiles := normalized
	if len(allFiles) == 0 {
		gitBaseSHA := baseSHA
		if isRootCommitBase(gitBaseSHA) {
			gitBaseSHA = GitEmptyTreeSHA
		}
		files, err := gitChangedFiles(workspacePath, event.RepoID, gitBaseSHA, targetSHA)
		if err != nil {
			return CommitDiff{}, fmt.Errorf("resolve changed files via git: %w", err)
		}
		allFiles = files
	}

	allFiles = filterTrackedChangedFiles(allFiles)
	if len(allFiles) == 0 {
		if baseSHA == "" {
			return CommitDiff{}, fmt.Errorf("no changed files in root commit %s", targetSHA)
		}
		return CommitDiff{}, fmt.Errorf("%w between %s and %s", ErrNoCodeFilesChanged, baseSHA, targetSHA)
	}

	outputBaseSHA := baseSHA
	if isRootCommitBase(outputBaseSHA) {
		outputBaseSHA = GitEmptyTreeSHA
	}

	return CommitDiff{
		RepoID:       event.RepoID,
		SnapshotID:   event.SnapshotID,
		BaseSHA:      outputBaseSHA,
		TargetSHA:    targetSHA,
		ChangedFiles: allFiles,
	}, nil
}

func filterTrackedChangedFiles(files []string) []string {
	out := make([]string, 0, len(files))
	for _, file := range files {
		if IsIgnoredPath(file) {
			continue
		}
		out = append(out, file)
	}
	return out
}

func normalizeChangedFiles(files []string) []string {
	if len(files) == 0 {
		return nil
	}
	out := make([]string, 0, len(files))
	for _, file := range files {
		file = strings.TrimSpace(file)
		if file == "" {
			continue
		}
		out = append(out, filepath.ToSlash(file))
	}
	return out
}

func filterCodeFiles(files []string) []string {
	out := make([]string, 0, len(files))
	for _, file := range files {
		if IsIgnoredPath(file) {
			continue
		}
		if isCodeFilePath(file) {
			out = append(out, file)
		}
	}
	return out
}

func isCodeFilePath(filePath string) bool {
	switch strings.ToLower(filepath.Ext(filePath)) {
	case ".go", ".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".rb", ".rs", ".dart", ".lua",
		".php", ".cs", ".cpp", ".c", ".h", ".html", ".htm", ".vue", ".css", ".scss", ".sass", ".less", ".sql":
		return true
	default:
		return false
	}
}

func defaultGitChangedFiles(workspacePath, repoID, baseSHA, targetSHA string) ([]string, error) {
	repoDir, err := WorkspaceDir(workspacePath, repoID)
	if err != nil {
		return nil, err
	}

	cmd := exec.Command("git", "-C", repoDir, "diff", "--name-only", baseSHA, targetSHA)
	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("git diff --name-only: %w", err)
	}

	lines := strings.Split(strings.TrimSpace(string(output)), "\n")
	return normalizeChangedFiles(lines), nil
}
