package testmark

import (
	"bytes"
	"unicode"
)

// NewHunk creates a new hunk with the given name, info string, and body.
// The body can be []byte or string - both are converted to []byte internally.
func NewHunk[T bytesLike](name, infoString string, body T) *Hunk {
	if name == "" || bytes.IndexFunc([]byte(name), unicode.IsSpace) >= 0 {
		panic("hunk name must not be empty and cannot contain whitespace")
	}

	// Convert body to []byte based on type
	var bodyBytes []byte
	switch v := any(body).(type) {
	case []byte:
		bodyBytes = v
	case string:
		bodyBytes = []byte(v)
	default:
		// This case should never happen due to type constraints, but just in case
		bodyBytes = []byte(any(body).(string))
	}

	return &Hunk{
		name:       name,
		infoString: infoString,
		body:       bodyBytes,
	}
}

// Patch applies the given hunks to the document and returns a new document.
func (d *Document) Patch(hunks ...*Hunk) *Document {
	// Create hunk map for efficient lookup
	newHunks := make(map[string]*Hunk, len(hunks))
	for _, hunk := range hunks {
		newHunks[hunk.name] = hunk
	}

	// Create new document structure
	newDoc := &Document{
		hunksByName: make(map[string]*Hunk),
	}

	// Build new content line by line
	var newLines [][]byte
	var leftOff int

	// Process existing hunks
	for _, info := range d.hunks {
		// Copy prose lines before this hunk
		newLines = append(newLines, d.lines[leftOff:info.lineStart]...)
		leftOff = info.lineEnd + 1

		// Determine what content to use
		var bodyLines [][]byte
		var infoString string
		var hunkToUse *Hunk

		if newHunk, exists := newHunks[info.hunk.name]; exists {
			// Use new hunk content
			bodyLines = bytes.Split(newHunk.body, sigilLineBreak)
			if len(newHunk.body) > 0 && newHunk.body[len(newHunk.body)-1] == '\n' {
				bodyLines = bodyLines[0 : len(bodyLines)-1]
			}
			infoString = newHunk.infoString
			hunkToUse = newHunk
			delete(newHunks, info.hunk.name) // Mark as processed
		} else {
			// Keep existing content
			bodyLines = d.lines[info.lineStart+2 : info.lineEnd]
			infoString = info.hunk.infoString
			hunkToUse = info.hunk
		}

		// Add hunk lines
		newLineStart := len(newLines)
		newLines = appendHunkLines(newLines, hunkToUse.name, infoString, bodyLines)
		newLineEnd := len(newLines)

		// Store in new document
		newInfo := hunkInfo{
			lineStart: newLineStart,
			lineEnd:   newLineEnd - 1,
			hunk:      hunkToUse,
		}
		newDoc.hunks = append(newDoc.hunks, newInfo)
		newDoc.hunksByName[hunkToUse.name] = hunkToUse
	}

	// Copy remaining prose lines
	newLines = append(newLines, d.lines[leftOff:]...)

	// Add any completely new hunks at the end
	for _, hunk := range hunks {
		if _, stillNew := newHunks[hunk.name]; stillNew {
			// Add blank line if needed
			if len(newLines) > 0 && len(newLines[len(newLines)-1]) > 0 {
				newLines = append(newLines, []byte{})
			}

			// Add the new hunk
			newLineStart := len(newLines)
			bodyLines := bytes.Split(hunk.body, sigilLineBreak)
			if len(hunk.body) > 0 && hunk.body[len(hunk.body)-1] == '\n' {
				bodyLines = bodyLines[0 : len(bodyLines)-1]
			}
			newLines = appendHunkLines(newLines, hunk.name, hunk.infoString, bodyLines)
			newLineEnd := len(newLines)

			// Store in new document
			newInfo := hunkInfo{
				lineStart: newLineStart,
				lineEnd:   newLineEnd - 1,
				hunk:      hunk,
			}
			newDoc.hunks = append(newDoc.hunks, newInfo)
			newDoc.hunksByName[hunk.name] = hunk

			// Add trailing blank line
			newLines = append(newLines, []byte{})
		}
	}

	// Clean up multiple trailing blank lines
	for len(newLines) > 1 && len(newLines[len(newLines)-1]) == 0 && len(newLines[len(newLines)-2]) == 0 {
		newLines = newLines[:len(newLines)-1]
	}

	// Rebuild document content
	newDoc.lines = newLines
	newDoc.original = bytes.Join(newLines, sigilLineBreak)

	// Build directory index
	buildDirIndex(newDoc)

	return newDoc
}

// UpdateHunk creates a new document with a single hunk updated.
// This is a convenience method for the common case of updating one hunk.
func (d *Document) UpdateHunk(name, content string) *Document {
	return d.UpdateHunkWithInfo(name, "", content)
}

// UpdateHunkWithInfo creates a new document with a single hunk updated, including info string.
func (d *Document) UpdateHunkWithInfo(name, infoString, content string) *Document {
	hunk := NewHunk(name, infoString, content)
	return d.Patch(hunk)
}

// PatchFromMap creates a new document by patching multiple hunks from a map.
// The map keys are hunk names, values are the new content (as strings).
func (d *Document) PatchFromMap(updates map[string]string) *Document {
	if len(updates) == 0 {
		return d
	}

	hunks := make([]*Hunk, 0, len(updates))
	for name, content := range updates {
		hunks = append(hunks, NewHunk(name, "", content))
	}
	return d.Patch(hunks...)
}

// appendHunkLines adds the testmark formatting around hunk content
func appendHunkLines(lines [][]byte, hunkName string, infoString string, bodyLines [][]byte) [][]byte {
	// Add comment line
	commentLine := append(sigilTestmark, append([]byte("("), append([]byte(hunkName), ')')...)...)
	lines = append(lines, commentLine)

	// Add code block start
	codeBlockStart := append(sigilCodeBlock, []byte(infoString)...)
	lines = append(lines, codeBlockStart)

	// Add body lines
	lines = append(lines, bodyLines...)

	// Add code block end
	lines = append(lines, sigilCodeBlock)

	return lines
}

// PatchAccumulator methods

// Add appends a hunk to the patch list.
func (pa *PatchAccumulator) Add(hunk *Hunk) {
	pa.patches = append(pa.patches, *hunk)
}

// Count returns the number of accumulated patches.
func (pa *PatchAccumulator) Count() int {
	return len(pa.patches)
}

// ApplyTo applies all accumulated patches to a document.
func (pa *PatchAccumulator) ApplyTo(doc *Document) *Document {
	patches := make([]*Hunk, len(pa.patches))
	for i, p := range pa.patches {
		copy := p
		patches[i] = &copy
	}
	return doc.Patch(patches...)
}
