package redact

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"regexp"

	"github.com/replicatedhq/troubleshoot/pkg/constants"
	"k8s.io/klog/v2"
)

type SingleLineRedactor struct {
	scan       *regexp.Regexp
	re         *regexp.Regexp
	maskText   string
	filePath   string
	redactName string
	isDefault  bool
}

var NEW_LINE = []byte{'\n'}

func NewSingleLineRedactor(re LineRedactor, maskText, path, name string, isDefault bool) (*SingleLineRedactor, error) {
	var scanCompiled *regexp.Regexp
	compiled, err := compileRegex(re.regex)
	if err != nil {
		return nil, err
	}

	if re.scan != "" {
		scanCompiled, err = compileRegex(re.scan)
		if err != nil {
			return nil, err
		}
	}

	return &SingleLineRedactor{scan: scanCompiled, re: compiled, maskText: maskText, filePath: path, redactName: name, isDefault: isDefault}, nil
}

// Redact processes the input reader line-by-line, applying redaction patterns.

// Unlike the previous implementation using bufio.Scanner, this now uses LineReader
// to preserve the exact newline structure of the input file. Lines that originally
// ended with \n will have \n added back, while lines without \n (like the last line
// of a file without a trailing newline, or binary files) will not have \n added.
// This ensures binary files and text files without trailing newlines are not corrupted.
func (r *SingleLineRedactor) Redact(input io.Reader, path string) io.Reader {
	out, writer := io.Pipe()

	go func() {
		var err error
		defer func() {
			if err == nil || err == io.EOF {
				writer.Close()
			} else {
				// Check if error is about line exceeding maximum size
				if errors.Is(err, bufio.ErrTooLong) {
					s := fmt.Sprintf("Error redacting %q. A line in the file exceeded %d MB max length", path, constants.SCANNER_MAX_SIZE/1024/1024)
					klog.V(2).Info(s)
				} else {
					klog.V(2).Info(fmt.Sprintf("Error redacting %q: %v", path, err))
				}
				writer.CloseWithError(err)
			}
		}()

		// Use LineReader instead of bufio.Scanner to track newline presence
		lineReader := NewLineReader(input)
		tokenizer := GetGlobalTokenizer()
		lineNum := 0

		for {
			line, hadNewline, readErr := lineReader.ReadLine()

			// Handle EOF with no content - we're done
			if readErr == io.EOF && len(line) == 0 {
				break
			}

			// We have content to process
			lineNum++

			// Determine if we should redact this line
			shouldRedact := true

			// Pre-filter: if scan is not nil, check if line matches scan by lowercasing it
			if r.scan != nil {
				lowerLine := bytes.ToLower(line)
				if !r.scan.Match(lowerLine) {
					shouldRedact = false
				}
			}

			// Check if line matches the main redaction pattern
			if shouldRedact && !r.re.Match(line) {
				shouldRedact = false
			}

			// Process the line (redact or pass through)
			var outputLine []byte
			if shouldRedact {
				// Line matches - perform redaction
				if tokenizer.IsEnabled() {
					// Use tokenized replacement - context comes from the redactor name
					context := r.redactName
					outputLine = getTokenizedReplacementPatternWithPath(r.re, line, context, r.filePath)
				} else {
					// Use original masking behavior
					substStr := []byte(getReplacementPattern(r.re, r.maskText))
					outputLine = r.re.ReplaceAll(line, substStr)
				}

				// Track redaction if content changed
				if !bytes.Equal(outputLine, line) {
					addRedaction(Redaction{
						RedactorName:      r.redactName,
						CharactersRemoved: len(line) - len(outputLine),
						Line:              lineNum,
						File:              r.filePath,
						IsDefaultRedactor: r.isDefault,
					})
				}
			} else {
				// No match - use original line
				outputLine = line
			}

			// Write the line
			err = writeBytes(writer, outputLine)
			if err != nil {
				return
			}
			// Only add newline if original line had one
			if hadNewline {
				err = writeBytes(writer, NEW_LINE)
				if err != nil {
					return
				}
			}

			// Check if we hit EOF after processing this line
			if readErr == io.EOF {
				break
			}
			// Check for non-EOF errors
			if readErr != nil {
				err = readErr
				return
			}
		}
	}()
	return out
}
