/*
Copyright © 2022-2026 Thomas von Dein

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package lib

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
	"strings"
	"text/template"

	"codeberg.org/scip/tablizer/cfg"
	"github.com/Masterminds/sprig/v3"
	"github.com/gookit/color"
	"github.com/olekukonko/tablewriter"
	"github.com/olekukonko/tablewriter/renderer"
	"github.com/olekukonko/tablewriter/tw"
	"gopkg.in/yaml.v3"
)

func printData(writer io.Writer, conf cfg.Config, data *Tabdata) {
	//  Sort   the  data  first,  before   headers+entries  are  being
	// reduced. That way the user can specify any valid column to sort
	// by, independently if it's being used for display or not.
	sortTable(conf, data)

	// put one or more columns into clipboard
	yankColumns(conf, data)

	// add numbers to headers and remove those we're not interested in
	numberizeAndReduceHeaders(conf, data)

	// remove unwanted columns, if any
	reduceColumns(conf, data)

	switch conf.OutputMode {
	case cfg.Extended:
		printExtendedData(writer, conf, data)
	case cfg.ASCII:
		printASCIIData(writer, conf, data)
	case cfg.Orgtbl:
		printOrgmodeData(writer, conf, data)
	case cfg.Markdown:
		printMarkdownData(writer, conf, data)
	case cfg.Shell:
		printShellData(writer, data)
	case cfg.Yaml:
		printYamlData(writer, data)
	case cfg.Json:
		printJsonData(writer, data)
	case cfg.CSV:
		printCSVData(writer, conf, data)
	case cfg.Template:
		printTemplateData(writer, conf, data)
	default:
		printASCIIData(writer, conf, data)
	}
}

func output(writer io.Writer, conf cfg.Config, str string) {
	_, err := fmt.Fprint(writer, colorizeOutput(conf, str))
	if err != nil {
		log.Fatalf("failed to print output: %s", err)
	}
}

/*
Emacs org-mode compatible table (also orgtbl-mode)
*/
func printOrgmodeData(writer io.Writer, conf cfg.Config, data *Tabdata) {
	tableString := &strings.Builder{}

	table := tablewriter.NewTable(tableString,
		tablewriter.WithRenderer(
			renderer.NewBlueprint(
				tw.Rendition{
					Borders: tw.Border{
						Left:   tw.On,
						Right:  tw.On,
						Top:    tw.On,
						Bottom: tw.On,
					},
					Settings: tw.Settings{
						Separators: tw.Separators{
							ShowHeader:     tw.On,
							ShowFooter:     tw.Off,
							BetweenRows:    tw.Off,
							BetweenColumns: 0,
						},
					},
					Symbols: tw.NewSymbols(tw.StyleASCII),
				})),

		tablewriter.WithConfig(
			tablewriter.Config{
				Header: tw.CellConfig{
					Formatting: tw.CellFormatting{
						Alignment:  tw.AlignLeft,
						AutoFormat: tw.Off,
					},
				},
				Row: tw.CellConfig{
					Formatting: tw.CellFormatting{
						Alignment: tw.AlignLeft,
					},
				},
			},
		),
	)

	if !conf.NoHeaders {
		table.Header(data.headers)
	}

	if err := table.Bulk(data.entries); err != nil {
		log.Fatalf("Failed to add data to table renderer: %s", err)
	}

	if err := table.Render(); err != nil {
		log.Fatalf("Failed to render table: %s", err)
	}

	output(writer, conf, color.Sprint(colorizeData(conf, tableString.String())))
}

/*
Markdown table
*/
func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) {
	tableString := &strings.Builder{}

	table := tablewriter.NewTable(tableString,
		tablewriter.WithRenderer(
			renderer.NewBlueprint(
				tw.Rendition{
					Borders: tw.Border{
						Left:   tw.On,
						Right:  tw.On,
						Top:    tw.Off,
						Bottom: tw.Off,
					},
					Settings: tw.Settings{
						Separators: tw.Separators{
							ShowHeader:     tw.On,
							ShowFooter:     tw.Off,
							BetweenRows:    tw.Off,
							BetweenColumns: 0,
						},
					},
					Symbols: tw.NewSymbols(tw.StyleMarkdown),
				})),

		tablewriter.WithConfig(
			tablewriter.Config{
				Header: tw.CellConfig{
					Formatting: tw.CellFormatting{
						Alignment:  tw.AlignLeft,
						AutoFormat: tw.Off,
					},
				},
				Row: tw.CellConfig{
					Formatting: tw.CellFormatting{
						Alignment: tw.AlignLeft,
					},
				},
			},
		),
	)

	if !conf.NoHeaders {
		table.Header(data.headers)
	}

	if err := table.Bulk(data.entries); err != nil {
		log.Fatalf("Failed to add data to table renderer: %s", err)
	}

	if err := table.Render(); err != nil {
		log.Fatalf("Failed to render table: %s", err)
	}

	output(writer, conf, color.Sprint(colorizeData(conf, tableString.String())))
}

/*
Simple ASCII table without any borders etc, just like the input we expect
*/
func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) {
	OFS := "  "
	if conf.OFS != "" {
		OFS = conf.OFS
	}

	tableString := &strings.Builder{}

	styleTSV := tw.NewSymbolCustom("space").WithColumn("\t")

	table := tablewriter.NewTable(tableString,
		tablewriter.WithRenderer(
			renderer.NewBlueprint(tw.Rendition{
				Borders: tw.BorderNone,
				Symbols: styleTSV,
				Settings: tw.Settings{
					Separators: tw.SeparatorsNone,
					Lines:      tw.LinesNone,
				},
			})),
		tablewriter.WithConfig(tablewriter.Config{
			Header: tw.CellConfig{
				Formatting: tw.CellFormatting{
					AutoFormat: tw.Off,
				},
				Padding: tw.CellPadding{Global: tw.Padding{Left: "", Right: OFS}},
			},
			Row: tw.CellConfig{
				Formatting: tw.CellFormatting{
					AutoWrap:  tw.WrapNone,
					Alignment: tw.AlignLeft,
				},
				Padding: tw.CellPadding{Global: tw.Padding{Right: OFS}},
			},

			Debug: true,
		}),
	)

	if !conf.NoHeaders {
		table.Header(data.headers)
	}

	if err := table.Bulk(data.entries); err != nil {
		log.Fatalf("Failed to add data to table renderer: %s", err)
	}

	if err := table.Render(); err != nil {
		log.Fatalf("Failed to render table: %s", err)
	}

	// we  need to trim  our output here, because  tablewriter appends
	// excess whitespace to our rows.
	cleanedString := &strings.Builder{}
	for _, row := range strings.Split(tableString.String(), "\n") {
		cleanedString.WriteString(strings.TrimSpace(row))
		cleanedString.WriteString("\n")
	}

	output(writer, conf, color.Sprint(colorizeData(conf, cleanedString.String())))
}

/*
We simulate the \x command of psql (the PostgreSQL client)
*/
func printExtendedData(writer io.Writer, conf cfg.Config, data *Tabdata) {
	// needed for data output
	format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader)
	out := ""

	if len(data.entries) > 0 {
		for _, entry := range data.entries {
			for i, value := range entry {
				out += color.Sprintf(format, data.headers[i], value)
			}

			out += "\n"
		}
	}

	output(writer, conf, colorizeData(conf, out))
}

/*
Shell output, ready to be eval'd. Just like FreeBSD stat(1)
*/
func printShellData(writer io.Writer, data *Tabdata) {
	out := ""

	if len(data.entries) > 0 {
		for _, entry := range data.entries {
			shentries := []string{}

			for idx, value := range entry {
				shentries = append(shentries, fmt.Sprintf("%s=\"%s\"",
					data.headers[idx], value))
			}

			out += strings.Join(shentries, " ") + "\n"
		}
	}

	// no colorization here
	output(writer, cfg.Config{}, out)
}

func printJsonData(writer io.Writer, data *Tabdata) {
	objlist := make([]map[string]any, len(data.entries))

	if len(data.entries) > 0 {
		for i, entry := range data.entries {
			obj := make(map[string]any, len(entry))

			for idx, value := range entry {
				num, err := strconv.Atoi(value)
				if err == nil {
					obj[data.headers[idx]] = num
				} else {
					obj[data.headers[idx]] = value
				}
			}

			objlist[i] = obj
		}
	}

	jsonstr, err := json.MarshalIndent(&objlist, "", "  ")

	if err != nil {
		log.Fatal(err)
	}

	output(writer, cfg.Config{}, string(jsonstr))
}

func printYamlData(writer io.Writer, data *Tabdata) {
	type Data struct {
		Entries []map[string]interface{} `yaml:"entries"`
	}

	yamlout := Data{}

	for _, entry := range data.entries {
		yamldata := map[string]interface{}{}

		for idx, entry := range entry {
			style := yaml.TaggedStyle

			_, err := strconv.Atoi(entry)
			if err != nil {
				style = yaml.DoubleQuotedStyle
			}

			yamldata[strings.ToLower(data.headers[idx])] =
				&yaml.Node{
					Kind:  yaml.ScalarNode,
					Style: style,
					Value: entry}
		}

		yamlout.Entries = append(yamlout.Entries, yamldata)
	}

	yamlstr, err := yaml.Marshal(&yamlout)

	if err != nil {
		log.Fatal(err)
	}

	output(writer, cfg.Config{}, string(yamlstr))
}

func printCSVData(writer io.Writer, conf cfg.Config, data *Tabdata) {
	OFS := ","
	if conf.OFS != "" {
		OFS = conf.OFS
	}

	csvout := csv.NewWriter(writer)
	csvout.Comma = []rune(OFS)[0]

	if err := csvout.Write(data.headers); err != nil {
		log.Fatalln("error writing record to csv:", err)
	}

	for _, entry := range data.entries {
		if err := csvout.Write(entry); err != nil {
			log.Fatalln("error writing record to csv:", err)
		}
	}

	csvout.Flush()

	if err := csvout.Error(); err != nil {
		log.Fatal(err)
	}
}

func printTemplateData(writer io.Writer, conf cfg.Config, data *Tabdata) {
	tmpl, err := template.New("printer").Funcs(sprig.TxtFuncMap()).Parse(conf.Template)
	if err != nil {
		log.Fatalf("failed to parse template: %s", err)
	}

	buf := strings.Builder{}

	for line, dict := range data.ToMap() {
		err = tmpl.Execute(&buf, dict)
		if err != nil {
			log.Fatalf("failed to execute template in line %d: %s", line, err)
		}

		buf.WriteString("\n")
	}

	if _, err := fmt.Fprintln(writer, buf.String()); err != nil {
		log.Fatalf("failed to print output: %s", err)
	}
}

func colorizeOutput(conf cfg.Config, input string) string {
	if len(conf.UseColorizers) > 0 && !conf.NoColor && color.IsConsole(os.Stdout) {
		for _, colorizer := range conf.UseColorizers {
			// colorize matching parts of the whole output with given color, if the terminal supports it
			// color may contain fg:bg or just fg. Color definitions see https://github.com/gookit/color
			input = colorizer.Search.ReplaceAllStringFunc(input, func(in string) string {
				col := colorizer.Replace
				if strings.Contains(col, ":") {
					parts := strings.Split(col, ":")
					return color.Sprintf("<fg=%s;bg=%s>%s</>", parts[0], parts[1], in)
				}

				return color.Sprintf("<fg=%s>%s</>", col, in)
			})
		}
	}

	return input
}
