package github

import (
	"bytes"
	"context"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	"github.com/github/github-mcp-server/pkg/inventory"
	"github.com/github/github-mcp-server/pkg/utils"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

// Ordered by preference when a response wrapper contains multiple arrays.
var primaryCSVRowKeys = []string{
	"items",
	"issues",
	"discussions",
	"categories",
	"labels",
	"alerts",
	"advisories",
	"notifications",
	"gists",
	"repositories",
	"commits",
	"branches",
	"tags",
	"releases",
	"users",
	"teams",
	"members",
	"projects",
	"nodes",
}

type csvOutputDocument struct {
	metadata map[string]string
	rows     []map[string]string
}

// withCSVOutput wraps the handler of every default-toolset list_* tool so that,
// at request time, it checks the csv_output feature flag and converts the JSON
// text response to CSV when enabled. The tool's schema, name, and scope are
// unchanged — only the response payload format differs.
func withCSVOutput(tools []inventory.ServerTool) []inventory.ServerTool {
	for i := range tools {
		if !isCSVOutputTool(tools[i]) {
			continue
		}
		tools[i].HandlerFunc = wrapHandlerWithCSVOutput(tools[i].HandlerFunc)
	}
	return tools
}

// isCSVOutputTool reports whether the given tool should have its handler
// wrapped to honor the csv_output feature flag. Wrapping happens at slice
// construction time, before the per-request feature-flag filter chooses which
// variant of a flag-gated tool to register, so flag-gated list_* tools are
// included on equal footing — only the live variant ever runs at request time.
func isCSVOutputTool(tool inventory.ServerTool) bool {
	if !tool.Toolset.Default {
		return false
	}
	return strings.HasPrefix(tool.Tool.Name, "list_")
}

func wrapHandlerWithCSVOutput(next inventory.HandlerFunc) inventory.HandlerFunc {
	return func(deps any) mcp.ToolHandler {
		handler := next(deps)
		csvDeps, _ := deps.(ToolDependencies)
		return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			result, err := handler(ctx, req)
			if err != nil || result == nil || result.IsError {
				return result, err
			}
			if csvDeps == nil || !csvDeps.IsFeatureEnabled(ctx, FeatureFlagCSVOutput) {
				return result, nil
			}
			return convertJSONTextResultToCSV(result), nil
		}
	}
}

func convertJSONTextResultToCSV(result *mcp.CallToolResult) *mcp.CallToolResult {
	if len(result.Content) != 1 {
		return utils.NewToolResultError("failed to convert response to CSV: expected a single text content response")
	}

	text, ok := result.Content[0].(*mcp.TextContent)
	if !ok {
		return utils.NewToolResultError("failed to convert response to CSV: expected a text content response")
	}

	csvText, err := jsonTextToCSV(text.Text)
	if err != nil {
		return utils.NewToolResultErrorFromErr("failed to convert response to CSV", err)
	}

	result.Content = []mcp.Content{&mcp.TextContent{Text: csvText}}
	result.StructuredContent = nil
	return result
}

func jsonTextToCSV(text string) (string, error) {
	decoder := json.NewDecoder(strings.NewReader(text))
	decoder.UseNumber()

	var value any
	if err := decoder.Decode(&value); err != nil {
		return "", fmt.Errorf("failed to unmarshal JSON text: %w", err)
	}

	doc := csvDocument(value)
	if len(doc.metadata) == 0 && len(doc.rows) == 0 {
		return "", nil
	}

	var buf bytes.Buffer
	writeCSVMetadata(&buf, doc.metadata)
	if len(doc.rows) == 0 {
		return buf.String(), nil
	}

	headers := csvHeaders(doc.rows)
	if len(headers) == 0 {
		return buf.String(), nil
	}

	writer := csv.NewWriter(&buf)
	if err := writer.Write(headers); err != nil {
		return "", fmt.Errorf("failed to write CSV header: %w", err)
	}

	for _, row := range doc.rows {
		record := make([]string, len(headers))
		for i, header := range headers {
			record[i] = row[header]
		}
		if err := writer.Write(record); err != nil {
			return "", fmt.Errorf("failed to write CSV row: %w", err)
		}
	}

	writer.Flush()
	if err := writer.Error(); err != nil {
		return "", fmt.Errorf("failed to flush CSV: %w", err)
	}
	return buf.String(), nil
}

func csvDocument(value any) csvOutputDocument {
	switch v := value.(type) {
	case []any:
		return csvOutputDocument{rows: csvRowsFromArray(v)}
	case map[string]any:
		if rows, metadata, ok := primaryRowsFromMap(v); ok {
			return csvOutputDocument{
				metadata: newFlattenedCSVRow(metadata),
				rows:     csvRowsFromArray(rows),
			}
		}
		return csvOutputDocument{rows: []map[string]string{newFlattenedCSVRow(v)}}
	default:
		return csvOutputDocument{rows: []map[string]string{scalarCSVRow(v)}}
	}
}

func primaryRowsFromMap(value map[string]any) ([]any, map[string]any, bool) {
	if rows, path, ok := primaryRowsAtCurrentLevel(value); ok {
		return rows, metadataWithoutPath(value, path), true
	}
	if rows, path, ok := primaryRowsOneLevelDown(value); ok {
		return rows, metadataWithoutPath(value, path), true
	}
	return nil, nil, false
}

func primaryRowsAtCurrentLevel(value map[string]any) ([]any, []string, bool) {
	if key, ok := preferredPrimaryRowKey(value); ok {
		rows, _ := value[key].([]any)
		return rows, []string{key}, true
	}
	if key, ok := singleArrayKey(value); ok {
		rows, _ := value[key].([]any)
		return rows, []string{key}, true
	}
	return nil, nil, false
}

func primaryRowsOneLevelDown(value map[string]any) ([]any, []string, bool) {
	var matchedRows []any
	var matchedPath []string
	for key, raw := range value {
		child, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		rows, path, ok := primaryRowsAtCurrentLevel(child)
		if !ok {
			continue
		}
		if matchedPath != nil {
			return nil, nil, false
		}
		matchedRows = rows
		matchedPath = append([]string{key}, path...)
	}
	if matchedPath == nil {
		return nil, nil, false
	}
	return matchedRows, matchedPath, true
}

func metadataWithoutPath(value map[string]any, path []string) map[string]any {
	metadata := make(map[string]any, len(value))
	for key, raw := range value {
		if key != path[0] {
			metadata[key] = raw
			continue
		}

		if len(path) == 1 {
			continue
		}
		child, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		childMetadata := metadataWithoutPath(child, path[1:])
		if len(childMetadata) > 0 {
			metadata[key] = childMetadata
		}
	}
	return metadata
}

func csvRowsFromArray(values []any) []map[string]string {
	if len(values) == 0 {
		return nil
	}

	rows := make([]map[string]string, 0, len(values))
	for _, value := range values {
		var row map[string]string
		switch v := value.(type) {
		case map[string]any:
			row = make(map[string]string)
			appendFlattenedCSVFields(row, v, "")
		default:
			row = scalarCSVRow(v)
		}
		rows = append(rows, row)
	}
	return rows
}

func writeCSVMetadata(buf *bytes.Buffer, metadata map[string]string) {
	if len(metadata) == 0 {
		return
	}

	headers := make([]string, 0, len(metadata))
	for header := range metadata {
		headers = append(headers, header)
	}
	sort.Strings(headers)

	for _, header := range headers {
		fmt.Fprintf(buf, "# %s: %s\n", header, normalizeCSVWhitespace(metadata[header]))
	}
	buf.WriteByte('\n')
}

func newFlattenedCSVRow(value map[string]any) map[string]string {
	row := make(map[string]string)
	appendFlattenedCSVFields(row, value, "")
	return row
}

func appendFlattenedCSVFields(row map[string]string, value map[string]any, prefix string) {
	if value == nil {
		return
	}

	for key, raw := range value {
		column := csvColumnName(prefix, key)
		switch v := raw.(type) {
		case map[string]any:
			appendFlattenedCSVFields(row, v, column)
		case []any:
			row[column] = csvArrayValue(v)
		default:
			row[column] = csvColumnValue(column, v)
		}
	}
}

func csvHeaders(rows []map[string]string) []string {
	headerSet := make(map[string]struct{})
	for _, row := range rows {
		for header := range row {
			headerSet[header] = struct{}{}
		}
	}

	headers := make([]string, 0, len(headerSet))
	for header := range headerSet {
		headers = append(headers, header)
	}
	sort.Strings(headers)
	return headers
}

func csvColumnName(prefix, key string) string {
	if prefix == "" {
		return key
	}
	return prefix + "." + key
}

func preferredPrimaryRowKey(value map[string]any) (string, bool) {
	for _, key := range primaryCSVRowKeys {
		if _, ok := value[key].([]any); ok {
			return key, true
		}
	}
	return "", false
}

func singleArrayKey(value map[string]any) (string, bool) {
	var arrayKey string
	for key, raw := range value {
		if _, ok := raw.([]any); !ok {
			continue
		}
		if arrayKey != "" {
			return "", false
		}
		arrayKey = key
	}
	if arrayKey == "" {
		return "", false
	}
	return arrayKey, true
}

func csvColumnValue(column string, value any) string {
	str := scalarCSVValue(value)
	if isBodyColumn(column) {
		return normalizeCSVWhitespace(str)
	}
	return str
}

func csvArrayValue(values []any) string {
	if len(values) == 0 {
		return ""
	}

	// Scalar arrays use semicolons for compactness. This is lossy if an
	// element contains a semicolon; use JSON mode when exact reconstruction matters.
	parts := make([]string, 0, len(values))
	for _, value := range values {
		switch value.(type) {
		case map[string]any, []any:
			encoded, err := json.Marshal(value)
			if err != nil {
				parts = append(parts, scalarCSVValue(value))
			} else {
				parts = append(parts, string(encoded))
			}
		default:
			parts = append(parts, scalarCSVValue(value))
		}
	}
	return strings.Join(parts, ";")
}

func scalarCSVRow(value any) map[string]string {
	return map[string]string{"value": scalarCSVValue(value)}
}

func scalarCSVValue(value any) string {
	switch v := value.(type) {
	case nil:
		return ""
	case string:
		return v
	case json.Number:
		return v.String()
	case bool:
		if v {
			return "true"
		}
		return "false"
	default:
		return fmt.Sprint(v)
	}
}

func isBodyColumn(column string) bool {
	return column == "body" || strings.HasSuffix(column, ".body")
}

func normalizeCSVWhitespace(value string) string {
	return strings.Join(strings.Fields(value), " ")
}
