package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/base64"
	"encoding/csv"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"unicode/utf8"

	"text/template"

	"github.com/google/uuid"
	"gopkg.in/yaml.v3"
	"ucl.lmika.dev/ucl"
)

type TextProcessor struct {
	Label       string
	Description string

	// Filters the supplied text. This is used for manipulating the text input.
	Filter TextFilter

	// Analyses the supplied text. This is used for extracting information from the text input. The result
	// will be displayed in the status bar. Multiple selected regions will be returned as a single string separated by line numbers.
	Analyze TextAnalyzer

	// FilterWithArg requests some input from the user. If the user supplies it, it will return a text filter
	// to process the input.
	FilterWithArg *FilterWithArg
}

type TextFilter func(ctx context.Context, input string) (resp TextFilterResponse, err error)
type TextAnalyzer func(ctx context.Context, input string) (resp string, err error)

type FilterWithArg struct {
	Label     string
	OnConfirm func(ctx context.Context, input string) (filter TextFilter, err error)
}

type TextFilterResponse struct {
	Output string
	Append bool
}

var TextFilters = map[string]TextProcessor{
	"upper-case": {
		Label: "String: To Upper Case",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			return TextFilterResponse{Output: strings.ToUpper(input)}, nil
		},
	},
	"lower-case": {
		Label: "String: To Lower Case",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			return TextFilterResponse{Output: strings.ToLower(input)}, nil
		},
	},
	"quote": {
		Label: "String: Quote",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			return TextFilterResponse{Output: strconv.Quote(input)}, nil
		},
	},
	"unquote": {
		Label: "String: Unquote",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			endNL := ""
			if strings.HasSuffix(input, "\n") {
				endNL = "\n"
			}

			out, err := strconv.Unquote(strings.TrimSpace(input))
			if err != nil {
				return TextFilterResponse{}, err
			}
			return TextFilterResponse{Output: out + endNL}, nil
		},
	},
	"join-lines-with-commas": {
		Label: "Lines: Join with Commas",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			return TextFilterResponse{Output: strings.Replace(input, "\n", ",", -1)}, nil
		},
	},
	"join-lines-with-spaces": {
		Label: "Lines: Join with Spaces",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			return TextFilterResponse{Output: strings.Replace(input, "\n", " ", -1)}, nil
		},
	},
	"split-lines-on-commas": {
		Label: "Lines: Split on Commas",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			return TextFilterResponse{Output: strings.Replace(input, ",", "\n", -1)}, nil
		},
	},
	"join-lines-with-semicolons": {
		Label: "Lines: Join with Semicolons",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			return TextFilterResponse{Output: strings.Replace(input, "\n", ";", -1)}, nil
		},
	},
	"split-lines-on-semicolon": {
		Label: "Lines: Split on Semicolons",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			return TextFilterResponse{Output: strings.Replace(input, ";", "\n", -1)}, nil
		},
	},
	"trim-space": {
		Label: "Lines: Trim Spaces",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			lines := strings.Split(input, "\n")
			for i, line := range lines {
				lines[i] = strings.TrimSpace(line)
			}
			result := strings.Join(lines, "\n")
			return TextFilterResponse{Output: result}, nil
		},
	},
	"compact-json": {
		Label: "JSON: Compact",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			var (
				dst    bytes.Buffer
				inBfr  bytes.Buffer
				outBfr bytes.Buffer
			)

			scnr := bufio.NewScanner(strings.NewReader(input))
			for scnr.Scan() {
				line := scnr.Text()
				inBfr.WriteString(line)
				inBfr.WriteString("\n")

				if err := json.Compact(&outBfr, inBfr.Bytes()); err == nil {
					dst.WriteString(outBfr.String())
					dst.WriteString("\n")
					inBfr.Reset()
					outBfr.Reset()
				}
			}

			return TextFilterResponse{Output: dst.String() + inBfr.String()}, nil
		},
	},
	"format-json": {
		Label: "JSON: Format",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			var dst bytes.Buffer

			scnr := bufio.NewScanner(strings.NewReader(input))
			for scnr.Scan() {
				line := scnr.Text()
				if err := json.Indent(&dst, []byte(line), "", "  "); err == nil {
					dst.WriteString("\n")
				} else {
					return TextFilterResponse{}, err
				}
			}

			return TextFilterResponse{Output: dst.String()}, nil
		},
	},
	"convert-json-to-yaml": {
		Label: "Convert: JSON to YAML",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			var data interface{}
			if err := json.Unmarshal([]byte(input), &data); err != nil {
				return TextFilterResponse{}, err
			}

			var dst bytes.Buffer
			if err := yaml.NewEncoder(&dst).Encode(data); err != nil {
				return TextFilterResponse{}, err
			}

			return TextFilterResponse{Output: dst.String()}, nil
		},
	},
	"csv-to-ascii-table": {
		Label: "Convert: CSV to ASCII Table",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			endNL := ""
			if strings.HasSuffix(input, "\n") {
				endNL = "\n"
			}

			reader := csv.NewReader(strings.NewReader(input))
			reader.FieldsPerRecord = -1
			records, err := reader.ReadAll()
			if err != nil {
				return TextFilterResponse{}, err
			}
			if len(records) == 0 {
				return TextFilterResponse{Output: input}, nil
			}

			var colWidths []int
			for _, record := range records {
				for i, cell := range record {
					if i >= len(colWidths) {
						colWidths = append(colWidths, 0)
					}
					colWidths[i] = max(colWidths[i], utf8.RuneCountInString(cell))
				}
			}

			writeRow := func(dst *strings.Builder, cells []string) {
				line := make([]string, len(colWidths))
				for i, width := range colWidths {
					cell := ""
					if i < len(cells) {
						cell = cells[i]
					}
					line[i] = cell + strings.Repeat(" ", width-utf8.RuneCountInString(cell))
				}
				dst.WriteString(strings.TrimRight(strings.Join(line, " "), " "))
				dst.WriteString("\n")
			}

			dashes := make([]string, len(colWidths))
			for i, width := range colWidths {
				dashes[i] = strings.Repeat("-", width)
			}
			dashLine := strings.Join(dashes, " ") + "\n"

			var dst strings.Builder
			writeRow(&dst, records[0])
			dst.WriteString(dashLine)
			for _, record := range records[1:] {
				writeRow(&dst, record)
			}
			dst.WriteString(dashLine)

			return TextFilterResponse{Output: strings.TrimSuffix(dst.String(), "\n") + endNL}, nil
		},
	},
	"convert-yaml-to-json": {
		Label: "Convert: YAML to JSON",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			var data interface{}
			if err := yaml.Unmarshal([]byte(input), &data); err != nil {
				return TextFilterResponse{}, err
			}

			jsonBytes, err := json.MarshalIndent(data, "", "  ")
			if err != nil {
				return TextFilterResponse{}, err
			}

			return TextFilterResponse{Output: string(jsonBytes)}, nil
		},
	},
	"jwt-decode": {
		Label: "JWT: Decode",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			tokens := strings.Split(input, ".")
			if len(tokens) != 3 {
				return TextFilterResponse{}, errors.New("invalid JWT format")
			}

			header, err := base64.RawURLEncoding.DecodeString(tokens[0])
			if err != nil {
				return TextFilterResponse{}, err
			}
			payload, err := base64.RawURLEncoding.DecodeString(tokens[1])
			if err != nil {
				return TextFilterResponse{}, err
			}

			var headerJSON, payloadJSON bytes.Buffer
			json.Indent(&headerJSON, header, "", "  ")
			json.Indent(&payloadJSON, payload, "", "  ")

			return TextFilterResponse{Output: headerJSON.String() + "\n" + payloadJSON.String()}, nil
		},
	},
	"base64-decode": {
		Label: "Base64: Decode",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			dst, err := base64.StdEncoding.DecodeString(input)
			if err != nil {
				return TextFilterResponse{}, err
			}

			return TextFilterResponse{Output: string(dst)}, nil
		},
	},
	"base64-encode": {
		Label: "Base64: Encode",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			dst := base64.StdEncoding.EncodeToString([]byte(input))
			return TextFilterResponse{Output: dst}, nil
		},
	},
	"lorem-ipsum": {
		Label: "Generate: Lorem Ipsum",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			return TextFilterResponse{
				Output: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " +
					"incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud " +
					"exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure " +
					"dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " +
					"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " +
					"mollit anim id est laborum.",
				Append: true,
			}, nil
		},
	},

	"uuid": {
		Label: "Generate: UUID v4",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			u, err := uuid.NewRandom()
			if err != nil {
				return TextFilterResponse{}, fmt.Errorf("failed to generate UUID: %w", err)
			}
			return TextFilterResponse{
				Output: u.String(),
				Append: true,
			}, nil
		},
	},

	"lines": {
		Label: "Lines: Count",
		Analyze: func(ctx context.Context, input string) (result string, err error) {
			lineCount := len(strings.Split(input, "\n"))
			return fmt.Sprintf("Lines = %v", lineCount), nil
		},
	},

	"template-each-line": {
		Label:       "Lines: Go Template…",
		Description: "Evaluates the input as a Go template and replaces each line with the result.",
		FilterWithArg: &FilterWithArg{
			Label: "Template",
			OnConfirm: func(ctx context.Context, prompt string) (filter TextFilter, err error) {
				tmpl, err := template.New("").Parse(prompt)
				if err != nil {
					return nil, err
				}

				return func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
					var (
						dst     bytes.Buffer
						dstLine bytes.Buffer
					)
					scnr := bufio.NewScanner(strings.NewReader(input))
					isFirst := true
					for scnr.Scan() {
						if isFirst {
							isFirst = false
						} else {
							dst.WriteString("\n")
						}

						dstLine.Reset()
						line := scnr.Text()
						if err := tmpl.Execute(&dstLine, line); err != nil {
							return TextFilterResponse{}, err
						}
						dst.WriteString(dstLine.String())
					}

					return TextFilterResponse{Output: dst.String()}, nil
				}, nil
			},
		},
	},

	"ucl-each-line": {
		Label:       "Lines: UCL…",
		Description: "Transform each line as the result of a UCL expression.",
		FilterWithArg: &FilterWithArg{
			Label: "UCL Expression",
			OnConfirm: func(ctx context.Context, prompt string) (filter TextFilter, err error) {
				spv := strPseudoVar{}

				uclCtx := uclInstFromContext(ctx)
				uclCtx.SetPseudoVar(".", &spv)

				return func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
					var (
						dst bytes.Buffer
					)
					scnr := bufio.NewScanner(strings.NewReader(input))
					isFirst := true
					for scnr.Scan() {
						if isFirst {
							isFirst = false
						} else {
							dst.WriteString("\n")
						}

						line := scnr.Text()
						spv.str = line

						if res, err := uclCtx.EvalString(ctx, prompt); err == nil {
							dst.WriteString(fmt.Sprint(res))
						} else if errors.Is(err, ucl.ErrNotConvertable) {
							dst.WriteString(line)
						} else {
							return TextFilterResponse{}, err
						}
					}

					return TextFilterResponse{Output: dst.String()}, nil
				}, nil
			},
		},
	},

	"sort-lines-asc": {
		Label: "Lines: Sort A-Z",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			lines := strings.Split(input, "\n")
			sort.Strings(lines)
			return TextFilterResponse{Output: strings.Join(lines, "\n")}, nil
		},
	},

	"sort-lines-desc": {
		Label: "Lines: Sort Z-A",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			lines := strings.Split(input, "\n")
			sort.Sort(sort.Reverse(sort.StringSlice(lines)))
			return TextFilterResponse{Output: strings.Join(lines, "\n")}, nil
		},
	},

	"unique-lines": {
		Label: "Lines: Unique",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			lines := strings.Split(input, "\n")
			seen := make(map[string]struct{}, len(lines))
			dst := make([]string, 0, len(lines))
			for _, line := range lines {
				if _, ok := seen[line]; ok {
					continue
				}
				seen[line] = struct{}{}
				dst = append(dst, line)
			}
			return TextFilterResponse{Output: strings.Join(dst, "\n")}, nil
		},
	},

	"remove-blank-lines": {
		Label: "Lines: Remove Blank Lines",
		Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
			var dst []string
			scnr := bufio.NewScanner(strings.NewReader(input))
			for scnr.Scan() {
				line := scnr.Text()
				if strings.TrimSpace(line) != "" {
					dst = append(dst, line)
				}
			}
			return TextFilterResponse{Output: strings.Join(dst, "\n")}, nil
		},
	},

	"keep-substring-matching": {
		Label:       "Lines: Keep Substring Matching…",
		Description: "Prompts for a regular expression, then replaces each line with the first matching substring, removing non-matching lines.",
		FilterWithArg: &FilterWithArg{
			Label: "Regular Expression",
			OnConfirm: func(ctx context.Context, prompt string) (filter TextFilter, err error) {
				re, err := regexp.Compile(prompt)
				if err != nil {
					return nil, err
				}

				return func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
					var dst []string
					scnr := bufio.NewScanner(strings.NewReader(input))
					for scnr.Scan() {
						line := scnr.Text()
						if match := re.FindString(line); match != "" {
							dst = append(dst, match)
						}
					}
					return TextFilterResponse{Output: strings.Join(dst, "\n")}, nil
				}, nil
			},
		},
	},
}

type strPseudoVar struct {
	str string
}

func (s strPseudoVar) Get(ctx context.Context) (any, error) {
	return s.str, nil
}
