package workspace

import (
	"bytes"
	"strings"

	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/filemode"
	fdiff "github.com/go-git/go-git/v5/plumbing/format/diff"
)

// reviewUnifiedDiffFromContent produces a unified diff from two content strings
// using go-git's diff infrastructure.
func reviewUnifiedDiffFromContent(oldContent, newContent string, oldPath, newPath string) (string, error) {
	if oldContent == newContent {
		return "", nil
	}
	if reviewContentTooLargeForDiff(oldContent, newContent) {
		return reviewDiffTooLargeMarker, nil
	}

	// Check for binary content
	if strings.Contains(oldContent, "\x00") || strings.Contains(newContent, "\x00") {
		return reviewDiffBinaryMarker, nil
	}

	// Build diff chunks using LCS-based line diff
	chunks := reviewComputeChunks(oldContent, newContent)
	if len(chunks) == 0 {
		return "", nil
	}

	// Build a minimal patch using go-git's unified encoder
	fp := &reviewContentFilePatch{
		oldPath: oldPath,
		newPath: newPath,
		chunks:  chunks,
		oldHash: plumbing.ComputeHash(plumbing.BlobObject, []byte(oldContent)),
		newHash: plumbing.ComputeHash(plumbing.BlobObject, []byte(newContent)),
	}

	p := &reviewContentPatch{filePatches: []fdiff.FilePatch{fp}}
	var buf bytes.Buffer
	ue := fdiff.NewUnifiedEncoder(&buf, fdiff.DefaultContextLines)
	if err := ue.Encode(p); err != nil {
		return "", err
	}
	return buf.String(), nil
}

func reviewContentTooLargeForDiff(oldContent, newContent string) bool {
	if len(oldContent) > reviewDiffMaxContentBytes || len(newContent) > reviewDiffMaxContentBytes {
		return true
	}
	oldLines := strings.Count(oldContent, "\n") + 1
	newLines := strings.Count(newContent, "\n") + 1
	if oldContent == "" {
		oldLines = 0
	}
	if newContent == "" {
		newLines = 0
	}
	if oldLines > reviewDiffMaxLines || newLines > reviewDiffMaxLines {
		return true
	}
	return oldLines*newLines > reviewDiffMaxCells
}

// reviewComputeChunks produces diff chunks from two strings using LCS-based line diff.
func reviewComputeChunks(oldContent, newContent string) []fdiff.Chunk {
	oldLines := reviewSplitIntoLines(oldContent)
	newLines := reviewSplitIntoLines(newContent)
	script := reviewEditScript(oldLines, newLines)
	return reviewCompactChunks(script)
}

func reviewSplitIntoLines(s string) []string {
	if s == "" {
		return nil
	}
	lines := strings.SplitAfter(s, "\n")
	if len(lines) > 0 && lines[len(lines)-1] == "" {
		lines = lines[:len(lines)-1]
	}
	return lines
}

type reviewEditOp int

const (
	reviewEditKeep   reviewEditOp = 0
	reviewEditInsert reviewEditOp = 1
	reviewEditDelete reviewEditOp = 2
)

type reviewEditEntry struct {
	op   reviewEditOp
	line string
}

// reviewEditScript computes an LCS-based edit script between two line slices.
func reviewEditScript(a, b []string) []reviewEditEntry {
	n := len(a)
	m := len(b)

	if n == 0 && m == 0 {
		return nil
	}
	if n == 0 {
		entries := make([]reviewEditEntry, m)
		for i, line := range b {
			entries[i] = reviewEditEntry{op: reviewEditInsert, line: line}
		}
		return entries
	}
	if m == 0 {
		entries := make([]reviewEditEntry, n)
		for i, line := range a {
			entries[i] = reviewEditEntry{op: reviewEditDelete, line: line}
		}
		return entries
	}

	// DP table for LCS length
	dp := make([][]int, n+1)
	for i := range dp {
		dp[i] = make([]int, m+1)
	}
	for i := 1; i <= n; i++ {
		for j := 1; j <= m; j++ {
			if a[i-1] == b[j-1] {
				dp[i][j] = dp[i-1][j-1] + 1
			} else if dp[i-1][j] >= dp[i][j-1] {
				dp[i][j] = dp[i-1][j]
			} else {
				dp[i][j] = dp[i][j-1]
			}
		}
	}

	// Backtrack to find edit script
	script := make([]reviewEditEntry, 0, n+m)
	i, j := n, m
	for i > 0 || j > 0 {
		if i > 0 && j > 0 && a[i-1] == b[j-1] {
			script = append(script, reviewEditEntry{op: reviewEditKeep, line: a[i-1]})
			i--
			j--
		} else if j > 0 && (i == 0 || dp[i][j-1] >= dp[i-1][j]) {
			script = append(script, reviewEditEntry{op: reviewEditInsert, line: b[j-1]})
			j--
		} else {
			script = append(script, reviewEditEntry{op: reviewEditDelete, line: a[i-1]})
			i--
		}
	}

	// Reverse to get correct order
	for left, right := 0, len(script)-1; left < right; left, right = left+1, right-1 {
		script[left], script[right] = script[right], script[left]
	}
	return script
}

// reviewCompactChunks converts edit entries into fdiff.Chunk slices by grouping
// consecutive operations of the same type.
func reviewCompactChunks(script []reviewEditEntry) []fdiff.Chunk {
	if len(script) == 0 {
		return nil
	}

	type opGroup struct {
		op   fdiff.Operation
		text strings.Builder
	}

	var groups []opGroup
	var current *opGroup

	for _, entry := range script {
		var op fdiff.Operation
		switch entry.op {
		case reviewEditKeep:
			op = fdiff.Equal
		case reviewEditInsert:
			op = fdiff.Add
		case reviewEditDelete:
			op = fdiff.Delete
		}
		if current == nil || current.op != op {
			groups = append(groups, opGroup{op: op})
			current = &groups[len(groups)-1]
		}
		current.text.WriteString(entry.line)
	}

	chunks := make([]fdiff.Chunk, len(groups))
	for i := range groups {
		chunks[i] = &reviewTextChunk{content: groups[i].text.String(), op: groups[i].op}
	}
	return chunks
}

// reviewTextChunk implements fdiff.Chunk.
type reviewTextChunk struct {
	content string
	op      fdiff.Operation
}

func (c *reviewTextChunk) Content() string       { return c.content }
func (c *reviewTextChunk) Type() fdiff.Operation { return c.op }

// reviewContentFilePatch implements fdiff.FilePatch for content-based diffs.
type reviewContentFilePatch struct {
	oldPath string
	newPath string
	chunks  []fdiff.Chunk
	oldHash plumbing.Hash
	newHash plumbing.Hash
}

func (fp *reviewContentFilePatch) IsBinary() bool        { return len(fp.chunks) == 0 }
func (fp *reviewContentFilePatch) Chunks() []fdiff.Chunk { return fp.chunks }
func (fp *reviewContentFilePatch) Files() (from fdiff.File, to fdiff.File) {
	if fp.oldPath != "" {
		from = &reviewContentFile{path: fp.oldPath, hash: fp.oldHash}
	}
	if fp.newPath != "" {
		to = &reviewContentFile{path: fp.newPath, hash: fp.newHash}
	}
	return
}

// reviewContentFile implements fdiff.File.
type reviewContentFile struct {
	path string
	hash plumbing.Hash
}

func (f *reviewContentFile) Hash() plumbing.Hash     { return f.hash }
func (f *reviewContentFile) Mode() filemode.FileMode { return filemode.Regular }
func (f *reviewContentFile) Path() string            { return f.path }
func (f *reviewContentFile) Empty() bool             { return false }

// reviewContentPatch implements fdiff.Patch.
type reviewContentPatch struct {
	filePatches []fdiff.FilePatch
}

func (p *reviewContentPatch) FilePatches() []fdiff.FilePatch { return p.filePatches }
func (p *reviewContentPatch) Message() string                { return "" }
