package main

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"io"
	"os"
	"regexp"
	"sort"
	"strings"
	"text/tabwriter"
	"time"

	tsql "github.com/SimonWaldherr/tinySQL"
	"github.com/SimonWaldherr/tinySQL/exporter"
	"github.com/SimonWaldherr/tinySQL/internal/engine"
)

// ============================================================================
// SQL Beautifier
// ============================================================================

// BeautifyOptions configures SQL formatting behavior.
type BeautifyOptions struct {
	IndentString   string // Indentation characters (default: "  ")
	Uppercase      bool   // Convert keywords to uppercase
	LineWidth      int    // Max line width before wrapping (0 = no limit)
	NewlineOnComma bool   // Put each column on new line in SELECT
}

// DefaultBeautifyOptions returns standard formatting options.
func DefaultBeautifyOptions() BeautifyOptions {
	return BeautifyOptions{
		IndentString:   "  ",
		Uppercase:      true,
		LineWidth:      80,
		NewlineOnComma: false,
	}
}

// SQLBeautifier formats SQL statements for readability.
type SQLBeautifier struct {
	opts BeautifyOptions
}

// NewSQLBeautifier creates a beautifier with the given options.
func NewSQLBeautifier(opts BeautifyOptions) *SQLBeautifier {
	return &SQLBeautifier{opts: opts}
}

// Beautify formats a SQL statement.
func (b *SQLBeautifier) Beautify(sql string) string {
	sql = normalizeWhitespace(sql)
	tokens := tokenizeSQL(sql)
	if b.opts.Uppercase {
		tokens = uppercaseKeywords(tokens)
	}
	return b.formatTokens(tokens)
}

// SQL keywords that should start on a new line
var majorKeywords = map[string]bool{
	"SELECT": true, "FROM": true, "WHERE": true, "AND": true, "OR": true,
	"ORDER": true, "GROUP": true, "HAVING": true, "LIMIT": true, "OFFSET": true,
	"JOIN": true, "LEFT": true, "RIGHT": true, "INNER": true, "OUTER": true,
	"ON": true, "UNION": true, "EXCEPT": true, "INTERSECT": true,
	"INSERT": true, "INTO": true, "VALUES": true,
	"UPDATE": true, "SET": true, "DELETE": true,
	"CREATE": true, "ALTER": true, "DROP": true, "TABLE": true,
	"WITH": true, "AS": true, "CASE": true, "WHEN": true, "THEN": true, "ELSE": true, "END": true,
}

var allKeywords = map[string]bool{
	"SELECT": true, "DISTINCT": true, "FROM": true, "WHERE": true,
	"AND": true, "OR": true, "NOT": true, "IN": true, "LIKE": true,
	"BETWEEN": true, "IS": true, "NULL": true, "TRUE": true, "FALSE": true,
	"ORDER": true, "BY": true, "ASC": true, "DESC": true,
	"GROUP": true, "HAVING": true, "LIMIT": true, "OFFSET": true,
	"JOIN": true, "LEFT": true, "RIGHT": true, "INNER": true, "OUTER": true, "ON": true,
	"UNION": true, "ALL": true, "EXCEPT": true, "INTERSECT": true,
	"INSERT": true, "INTO": true, "VALUES": true,
	"UPDATE": true, "SET": true, "DELETE": true,
	"CREATE": true, "ALTER": true, "DROP": true, "TABLE": true, "INDEX": true, "VIEW": true,
	"PRIMARY": true, "KEY": true, "FOREIGN": true, "REFERENCES": true, "UNIQUE": true,
	"INT": true, "TEXT": true, "FLOAT": true, "BOOL": true, "JSON": true, "DATETIME": true,
	"WITH": true, "AS": true, "CASE": true, "WHEN": true, "THEN": true, "ELSE": true, "END": true,
	"COUNT": true, "SUM": true, "AVG": true, "MIN": true, "MAX": true,
	"COALESCE": true, "NULLIF": true, "CAST": true,
}

func normalizeWhitespace(s string) string {
	re := regexp.MustCompile(`\s+`)
	return strings.TrimSpace(re.ReplaceAllString(s, " "))
}

type sqlToken struct {
	typ   string
	value string
}

// skipWhitespace skips whitespace characters
func skipWhitespace(sql string, i int) int {
	for i < len(sql) && (sql[i] == ' ' || sql[i] == '\t' || sql[i] == '\n') {
		i++
	}
	return i
}

// tokenizeSingleLineComment tokenizes a single-line comment (-- ...)
func tokenizeSingleLineComment(sql string, i int) (sqlToken, int) {
	j := i
	for j < len(sql) && sql[j] != '\n' {
		j++
	}
	return sqlToken{"comment", sql[i:j]}, j
}

// tokenizeMultiLineComment tokenizes a multi-line comment (/* ... */)
func tokenizeMultiLineComment(sql string, i int) (sqlToken, int) {
	j := i + 2
	for j+1 < len(sql) && (sql[j] != '*' || sql[j+1] != '/') {
		j++
	}
	if j+1 < len(sql) {
		j += 2
	}
	return sqlToken{"comment", sql[i:j]}, j
}

// tokenizeString tokenizes a string literal ('...')
func tokenizeString(sql string, i int) (sqlToken, int) {
	j := i + 1
	for j < len(sql) && sql[j] != '\'' {
		if sql[j] == '\\' && j+1 < len(sql) {
			j++
		}
		j++
	}
	if j < len(sql) {
		j++
	}
	return sqlToken{"string", sql[i:j]}, j
}

// tokenizeNumber tokenizes a numeric literal
func tokenizeNumber(sql string, i int) (sqlToken, int) {
	j := i
	for j < len(sql) && (sql[j] >= '0' && sql[j] <= '9' || sql[j] == '.') {
		j++
	}
	return sqlToken{"number", sql[i:j]}, j
}

// tokenizeIdentOrKeyword tokenizes an identifier or keyword
func tokenizeIdentOrKeyword(sql string, i int) (sqlToken, int) {
	j := i
	for j < len(sql) && isIdentChar(sql[j]) {
		j++
	}
	word := sql[i:j]
	if allKeywords[strings.ToUpper(word)] {
		return sqlToken{"keyword", word}, j
	}
	return sqlToken{"ident", word}, j
}

func tokenizeSQL(sql string) []sqlToken {
	var tokens []sqlToken
	i := 0
	for i < len(sql) {
		// Skip whitespace
		if sql[i] == ' ' || sql[i] == '\t' || sql[i] == '\n' {
			i = skipWhitespace(sql, i)
			continue
		}

		// Single-line comment
		if i+1 < len(sql) && sql[i] == '-' && sql[i+1] == '-' {
			tok, nextIdx := tokenizeSingleLineComment(sql, i)
			tokens = append(tokens, tok)
			i = nextIdx
			continue
		}

		// Multi-line comment
		if i+1 < len(sql) && sql[i] == '/' && sql[i+1] == '*' {
			tok, nextIdx := tokenizeMultiLineComment(sql, i)
			tokens = append(tokens, tok)
			i = nextIdx
			continue
		}

		// String literal
		if sql[i] == '\'' {
			tok, nextIdx := tokenizeString(sql, i)
			tokens = append(tokens, tok)
			i = nextIdx
			continue
		}

		// Number
		if sql[i] >= '0' && sql[i] <= '9' {
			tok, nextIdx := tokenizeNumber(sql, i)
			tokens = append(tokens, tok)
			i = nextIdx
			continue
		}

		// Identifier or keyword
		if isIdentStart(sql[i]) {
			tok, nextIdx := tokenizeIdentOrKeyword(sql, i)
			tokens = append(tokens, tok)
			i = nextIdx
			continue
		}

		// Symbol
		tokens = append(tokens, sqlToken{"symbol", string(sql[i])})
		i++
	}
	return tokens
}

func isIdentStart(c byte) bool {
	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}

func isIdentChar(c byte) bool {
	return isIdentStart(c) || (c >= '0' && c <= '9') || c == '.'
}

func uppercaseKeywords(tokens []sqlToken) []sqlToken {
	for i := range tokens {
		if tokens[i].typ == "keyword" {
			tokens[i].value = strings.ToUpper(tokens[i].value)
		}
	}
	return tokens
}

func (b *SQLBeautifier) formatTokens(tokens []sqlToken) string {
	var sb strings.Builder
	indent := 0
	newLine := true

	for i, tok := range tokens {
		upper := strings.ToUpper(tok.value)

		if tok.typ == "keyword" && majorKeywords[upper] {
			switch upper {
			case "SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "WITH":
				if i > 0 {
					sb.WriteString("\n")
				}
				newLine = true
			case "FROM", "WHERE", "ORDER", "GROUP", "HAVING", "LIMIT", "OFFSET", "SET", "VALUES":
				sb.WriteString("\n")
				newLine = true
			case "AND", "OR":
				sb.WriteString("\n")
				indent = 1
				newLine = true
			case "JOIN", "LEFT", "RIGHT", "INNER":
				sb.WriteString("\n")
				newLine = true
			case "UNION", "EXCEPT", "INTERSECT":
				sb.WriteString("\n\n")
				indent = 0
				newLine = true
			}
		}

		switch tok.value {
		case "(":
			indent++
		case ")":
			indent--
			if indent < 0 {
				indent = 0
			}
		}

		if newLine && tok.value != "(" && tok.value != ")" {
			for j := 0; j < indent; j++ {
				sb.WriteString(b.opts.IndentString)
			}
			newLine = false
		}

		sb.WriteString(tok.value)

		if i+1 < len(tokens) {
			next := tokens[i+1]
			if tok.value != "(" && next.value != ")" && next.value != "," && tok.value != "." && next.value != "." {
				sb.WriteString(" ")
			}
		}
	}

	return strings.TrimSpace(sb.String())
}

// ============================================================================
// Schema Browser
// ============================================================================

// SchemaBrowser provides schema inspection capabilities.
type SchemaBrowser struct {
	db *tsql.DB
}

// NewSchemaBrowser creates a schema browser for the given database.
func NewSchemaBrowser(db *tsql.DB) *SchemaBrowser {
	return &SchemaBrowser{db: db}
}

// ListTables returns all table names in a tenant.
func (sb *SchemaBrowser) ListTables(tenant string) []string {
	tables := sb.db.ListTables(tenant)
	names := make([]string, len(tables))
	for i, t := range tables {
		names[i] = t.Name
	}
	sort.Strings(names)
	return names
}

// TableInfo holds table metadata.
type TableInfo struct {
	Name     string
	Columns  []ColumnInfo
	RowCount int
}

// ColumnInfo holds column metadata.
type ColumnInfo struct {
	Name     string
	Type     string
	Nullable bool
	Primary  bool
}

// DescribeTable returns detailed info about a table.
func (sb *SchemaBrowser) DescribeTable(tenant, table string) (*TableInfo, error) {
	t, err := sb.db.Get(tenant, table)
	if err != nil || t == nil {
		return nil, fmt.Errorf("table %q not found in tenant %q", table, tenant)
	}

	info := &TableInfo{
		Name:     table,
		RowCount: len(t.Rows),
	}

	for _, col := range t.Cols {
		info.Columns = append(info.Columns, ColumnInfo{
			Name:     col.Name,
			Type:     col.Type.String(),
			Nullable: col.Constraint != tsql.PrimaryKey,
			Primary:  col.Constraint == tsql.PrimaryKey,
		})
	}

	return info, nil
}

// PrintSchema prints a formatted schema listing.
func (sb *SchemaBrowser) PrintSchema(tenant string, w *tabwriter.Writer) {
	tables := sb.ListTables(tenant)
	fmt.Fprintf(w, "Schema: %s\n", tenant)
	fmt.Fprintf(w, "Tables: %d\n\n", len(tables))

	for _, table := range tables {
		info, err := sb.DescribeTable(tenant, table)
		if err != nil {
			continue
		}
		fmt.Fprintf(w, "TABLE %s (%d rows)\n", info.Name, info.RowCount)
		fmt.Fprintf(w, "  %-20s\t%-10s\t%s\n", "Column", "Type", "Flags")
		fmt.Fprintf(w, "  %-20s\t%-10s\t%s\n", "------", "----", "-----")
		for _, col := range info.Columns {
			flags := ""
			if col.Primary {
				flags += "PK "
			}
			if !col.Nullable {
				flags += "NOT NULL"
			}
			fmt.Fprintf(w, "  %-20s\t%-10s\t%s\n", col.Name, col.Type, flags)
		}
		fmt.Fprintln(w)
	}
	w.Flush()
}

// ============================================================================
// Query History
// ============================================================================

// QueryHistoryEntry records a single query execution.
type QueryHistoryEntry struct {
	ID        int
	SQL       string
	Timestamp time.Time
	Duration  time.Duration
	RowCount  int
	Error     string
}

// QueryHistory tracks executed queries.
type QueryHistory struct {
	entries []QueryHistoryEntry
	maxSize int
	nextID  int
}

// NewQueryHistory creates a history tracker with max entries.
func NewQueryHistory(maxSize int) *QueryHistory {
	return &QueryHistory{
		entries: make([]QueryHistoryEntry, 0, maxSize),
		maxSize: maxSize,
		nextID:  1,
	}
}

// Add records a new query execution.
func (qh *QueryHistory) Add(sql string, duration time.Duration, rowCount int, err error) {
	entry := QueryHistoryEntry{
		ID:        qh.nextID,
		SQL:       sql,
		Timestamp: time.Now(),
		Duration:  duration,
		RowCount:  rowCount,
	}
	if err != nil {
		entry.Error = err.Error()
	}
	qh.nextID++

	qh.entries = append(qh.entries, entry)
	if len(qh.entries) > qh.maxSize {
		qh.entries = qh.entries[1:]
	}
}

// Last returns the last n entries.
func (qh *QueryHistory) Last(n int) []QueryHistoryEntry {
	if n > len(qh.entries) {
		n = len(qh.entries)
	}
	start := len(qh.entries) - n
	result := make([]QueryHistoryEntry, n)
	copy(result, qh.entries[start:])
	return result
}

// Search finds entries matching a pattern.
func (qh *QueryHistory) Search(pattern string) []QueryHistoryEntry {
	var results []QueryHistoryEntry
	lower := strings.ToLower(pattern)
	for _, e := range qh.entries {
		if strings.Contains(strings.ToLower(e.SQL), lower) {
			results = append(results, e)
		}
	}
	return results
}

// PrintHistory displays query history in a formatted table.
func (qh *QueryHistory) PrintHistory(w *tabwriter.Writer, entries []QueryHistoryEntry) {
	fmt.Fprintf(w, "ID\tTime\tDuration\tRows\tStatus\tSQL\n")
	fmt.Fprintf(w, "--\t----\t--------\t----\t------\t---\n")
	for _, e := range entries {
		status := "OK"
		if e.Error != "" {
			status = "ERR"
		}
		sql := e.SQL
		if len(sql) > 50 {
			sql = sql[:47] + "..."
		}
		fmt.Fprintf(w, "%d\t%s\t%v\t%d\t%s\t%s\n",
			e.ID, e.Timestamp.Format("15:04:05"), e.Duration, e.RowCount, status, sql)
	}
	w.Flush()
}

// ============================================================================
// Query Explain
// ============================================================================

// QueryPlan represents a simple query execution plan.
type QueryPlan struct {
	Steps []PlanStep
}

// PlanStep is a single step in the query plan.
type PlanStep struct {
	Operation string
	Object    string
	Rows      string
	Cost      string
	Details   string
}

// ExplainQuery generates a simple query plan.
func ExplainQuery(sql string) (*QueryPlan, error) {
	stmt, err := tsql.ParseSQL(sql)
	if err != nil {
		return nil, err
	}

	plan := &QueryPlan{}

	switch s := stmt.(type) {
	case *engine.Select:
		if s.From.Table != "" {
			plan.Steps = append(plan.Steps, PlanStep{
				Operation: "TABLE SCAN",
				Object:    s.From.Table,
				Cost:      "low",
				Details:   "Sequential scan of table",
			})
		}

		for _, join := range s.Joins {
			joinTypeStr := "INNER"
			switch join.Type {
			case engine.JoinLeft:
				joinTypeStr = "LEFT"
			case engine.JoinRight:
				joinTypeStr = "RIGHT"
			}
			plan.Steps = append(plan.Steps, PlanStep{
				Operation: "NESTED LOOP JOIN",
				Object:    join.Right.Table,
				Cost:      "medium",
				Details:   fmt.Sprintf("%s join", joinTypeStr),
			})
		}

		if s.Where != nil {
			plan.Steps = append(plan.Steps, PlanStep{
				Operation: "FILTER",
				Object:    "-",
				Cost:      "low",
				Details:   "Apply WHERE conditions",
			})
		}

		if len(s.GroupBy) > 0 {
			plan.Steps = append(plan.Steps, PlanStep{
				Operation: "AGGREGATE",
				Object:    "-",
				Cost:      "medium",
				Details:   "Group and aggregate",
			})
		}

		if len(s.OrderBy) > 0 {
			plan.Steps = append(plan.Steps, PlanStep{
				Operation: "SORT",
				Object:    "-",
				Cost:      "medium-high",
				Details:   "Sort results",
			})
		}

		if s.Limit != nil || s.Offset != nil {
			plan.Steps = append(plan.Steps, PlanStep{
				Operation: "LIMIT/OFFSET",
				Object:    "-",
				Cost:      "low",
				Details:   "Apply row limits",
			})
		}

		plan.Steps = append(plan.Steps, PlanStep{
			Operation: "PROJECT",
			Object:    "-",
			Cost:      "low",
			Details:   fmt.Sprintf("Return %d columns", len(s.Projs)),
		})

	case *engine.Insert:
		plan.Steps = append(plan.Steps, PlanStep{
			Operation: "INSERT",
			Object:    s.Table,
			Cost:      "low",
			Details:   fmt.Sprintf("Insert %d row(s)", len(s.Rows)),
		})

	case *engine.Update:
		plan.Steps = append(plan.Steps, PlanStep{
			Operation: "TABLE SCAN",
			Object:    s.Table,
			Cost:      "low",
		})
		plan.Steps = append(plan.Steps, PlanStep{
			Operation: "UPDATE",
			Object:    s.Table,
			Cost:      "low",
			Details:   fmt.Sprintf("Update %d columns", len(s.Sets)),
		})

	case *engine.Delete:
		plan.Steps = append(plan.Steps, PlanStep{
			Operation: "TABLE SCAN",
			Object:    s.Table,
			Cost:      "low",
		})
		plan.Steps = append(plan.Steps, PlanStep{
			Operation: "DELETE",
			Object:    s.Table,
			Cost:      "low",
		})
	}

	return plan, nil
}

// PrintPlan displays a query plan.
func PrintPlan(plan *QueryPlan, w *tabwriter.Writer) {
	fmt.Fprintf(w, "Step\tOperation\tObject\tCost\tDetails\n")
	fmt.Fprintf(w, "----\t---------\t------\t----\t-------\n")
	for i, step := range plan.Steps {
		fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n",
			i+1, step.Operation, step.Object, step.Cost, step.Details)
	}
	w.Flush()
}

// ============================================================================
// Data Export
// ============================================================================

// ExportFormat specifies output format.
type ExportFormat string

const (
	FormatCSV    ExportFormat = "csv"
	FormatJSON   ExportFormat = "json"
	FormatNDJSON ExportFormat = "ndjson"
	FormatSQL    ExportFormat = "sql"
)

// Exporter exports query results to various formats.
type Exporter struct {
	format ExportFormat
}

// NewExporter creates an exporter for the given format.
func NewExporter(format ExportFormat) *Exporter {
	return &Exporter{format: format}
}

// Export writes result set to writer.
func (e *Exporter) Export(rs *tsql.ResultSet, tableName string, w io.Writer) error {
	switch e.format {
	case FormatCSV:
		return e.exportCSV(rs, w)
	case FormatJSON:
		return e.exportJSON(rs, w)
	case FormatNDJSON:
		return exporter.ExportNDJSON(w, rs, exporter.Options{})
	case FormatSQL:
		return exporter.ExportSQL(w, rs, tableName)
	default:
		return fmt.Errorf("unknown format: %s", e.format)
	}
}

func (e *Exporter) exportCSV(rs *tsql.ResultSet, w io.Writer) error {
	return exporter.ExportCSV(w, rs, exporter.Options{})
}

func (e *Exporter) exportJSON(rs *tsql.ResultSet, w io.Writer) error {
	return exporter.ExportJSON(w, rs, exporter.Options{PrettyJSON: true})
}

// ============================================================================
// Query Templates
// ============================================================================

// QueryTemplate is a reusable query snippet.
type QueryTemplate struct {
	Name        string
	Description string
	SQL         string
	Parameters  []string
}

// CommonTemplates returns built-in query templates.
func CommonTemplates() []QueryTemplate {
	return []QueryTemplate{
		{
			Name:        "select_all",
			Description: "Select all rows from a table",
			SQL:         "SELECT * FROM {table}",
			Parameters:  []string{"table"},
		},
		{
			Name:        "select_top",
			Description: "Select top N rows from a table",
			SQL:         "SELECT * FROM {table} LIMIT {limit}",
			Parameters:  []string{"table", "limit"},
		},
		{
			Name:        "select_where",
			Description: "Select with WHERE condition",
			SQL:         "SELECT * FROM {table} WHERE {column} = {value}",
			Parameters:  []string{"table", "column", "value"},
		},
		{
			Name:        "count_rows",
			Description: "Count rows in a table",
			SQL:         "SELECT COUNT(*) AS cnt FROM {table}",
			Parameters:  []string{"table"},
		},
		{
			Name:        "group_count",
			Description: "Count rows grouped by column",
			SQL:         "SELECT {column}, COUNT(*) AS cnt FROM {table} GROUP BY {column} ORDER BY cnt DESC",
			Parameters:  []string{"table", "column"},
		},
		{
			Name:        "distinct_values",
			Description: "Get distinct values in a column",
			SQL:         "SELECT DISTINCT {column} FROM {table} ORDER BY {column}",
			Parameters:  []string{"table", "column"},
		},
		{
			Name:        "join_tables",
			Description: "Join two tables",
			SQL:         "SELECT * FROM {table1} t1 JOIN {table2} t2 ON t1.{key1} = t2.{key2}",
			Parameters:  []string{"table1", "table2", "key1", "key2"},
		},
		{
			Name:        "insert_row",
			Description: "Insert a single row",
			SQL:         "INSERT INTO {table} ({columns}) VALUES ({values})",
			Parameters:  []string{"table", "columns", "values"},
		},
		{
			Name:        "update_row",
			Description: "Update rows matching condition",
			SQL:         "UPDATE {table} SET {column} = {value} WHERE {condition}",
			Parameters:  []string{"table", "column", "value", "condition"},
		},
		{
			Name:        "delete_rows",
			Description: "Delete rows matching condition",
			SQL:         "DELETE FROM {table} WHERE {condition}",
			Parameters:  []string{"table", "condition"},
		},
	}
}

// ApplyTemplate substitutes parameters into a template.
func ApplyTemplate(tmpl QueryTemplate, params map[string]string) string {
	result := tmpl.SQL
	for k, v := range params {
		result = strings.ReplaceAll(result, "{"+k+"}", v)
	}
	return result
}

// ============================================================================
// SQL Validator
// ============================================================================

// ValidationResult holds the result of SQL validation.
type ValidationResult struct {
	Valid    bool
	Error    string
	Warnings []string
	SQLType  string
}

// ValidateSQL checks if SQL is syntactically correct.
func ValidateSQL(sql string) ValidationResult {
	result := ValidationResult{Valid: true}

	stmt, err := tsql.ParseSQL(sql)
	if err != nil {
		result.Valid = false
		result.Error = err.Error()
		return result
	}

	switch stmt.(type) {
	case *engine.Select:
		result.SQLType = "SELECT"
	case *engine.Insert:
		result.SQLType = "INSERT"
	case *engine.Update:
		result.SQLType = "UPDATE"
	case *engine.Delete:
		result.SQLType = "DELETE"
	case *engine.CreateTable:
		result.SQLType = "CREATE TABLE"
	case *engine.DropTable:
		result.SQLType = "DROP TABLE"
	default:
		result.SQLType = "UNKNOWN"
	}

	upperSQL := strings.ToUpper(sql)

	if strings.Contains(upperSQL, "SELECT *") {
		result.Warnings = append(result.Warnings, "Consider specifying columns instead of SELECT *")
	}

	if strings.Contains(upperSQL, "DELETE FROM") && !strings.Contains(upperSQL, "WHERE") {
		result.Warnings = append(result.Warnings, "DELETE without WHERE will delete all rows!")
	}

	if strings.Contains(upperSQL, "UPDATE") && !strings.Contains(upperSQL, "WHERE") {
		result.Warnings = append(result.Warnings, "UPDATE without WHERE will update all rows!")
	}

	return result
}

// ============================================================================
// SQL Linter (multi-rule analysis)
// ============================================================================

// LintRule defines a single lint check.
type LintRule struct {
	ID       string // e.g. "L001"
	Name     string
	Severity string // "error", "warning", "info"
}

// LintIssue is a single finding from the linter.
type LintIssue struct {
	Rule    LintRule
	Message string
	SQL     string // the offending SQL snippet
}

// LintResult holds all findings from linting a SQL file.
type LintResult struct {
	Issues     []LintIssue
	Statements int
}

var lintRules = []LintRule{
	{ID: "L001", Name: "select-star", Severity: "warning"},
	{ID: "L002", Name: "missing-where-delete", Severity: "error"},
	{ID: "L003", Name: "missing-where-update", Severity: "error"},
	{ID: "L004", Name: "implicit-cross-join", Severity: "warning"},
	{ID: "L005", Name: "order-by-ordinal", Severity: "info"},
	{ID: "L006", Name: "nested-subquery-depth", Severity: "warning"},
	{ID: "L007", Name: "inconsistent-keyword-case", Severity: "info"},
	{ID: "L009", Name: "syntax-error", Severity: "error"},
}

// LintSQL analyzes SQL text (possibly multi-statement) and returns lint issues.
func LintSQL(sqlText string) LintResult {
	result := LintResult{}

	// Split into statements
	stmts := splitStatements(sqlText)
	result.Statements = len(stmts)

	for _, stmt := range stmts {
		stmt = strings.TrimSpace(stmt)
		if stmt == "" {
			continue
		}
		upper := strings.ToUpper(stmt)

		// L009: Syntax check
		vr := ValidateSQL(stmt)
		if !vr.Valid {
			result.Issues = append(result.Issues, LintIssue{
				Rule:    findRule("L009"),
				Message: "Syntax error: " + vr.Error,
				SQL:     truncateSQL(stmt, 80),
			})
			continue
		}

		// L001: SELECT *
		if strings.Contains(upper, "SELECT *") && !strings.Contains(upper, "COUNT(*)") {
			result.Issues = append(result.Issues, LintIssue{
				Rule:    findRule("L001"),
				Message: "Avoid SELECT * — specify columns explicitly for clarity and performance",
				SQL:     truncateSQL(stmt, 80),
			})
		}

		// L002: DELETE without WHERE
		if strings.HasPrefix(upper, "DELETE") && !strings.Contains(upper, "WHERE") {
			result.Issues = append(result.Issues, LintIssue{
				Rule:    findRule("L002"),
				Message: "DELETE without WHERE clause will remove all rows",
				SQL:     truncateSQL(stmt, 80),
			})
		}

		// L003: UPDATE without WHERE
		if strings.HasPrefix(upper, "UPDATE") && !strings.Contains(upper, "WHERE") {
			result.Issues = append(result.Issues, LintIssue{
				Rule:    findRule("L003"),
				Message: "UPDATE without WHERE clause will modify all rows",
				SQL:     truncateSQL(stmt, 80),
			})
		}

		// L004: Implicit cross join (FROM a, b)
		if strings.HasPrefix(upper, "SELECT") && strings.Contains(upper, "FROM") {
			fromIdx := strings.Index(upper, "FROM")
			if fromIdx >= 0 {
				rest := upper[fromIdx+4:]
				// Find the clause boundary
				for _, kw := range []string{"WHERE", "GROUP", "ORDER", "LIMIT", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "CROSS"} {
					if idx := strings.Index(rest, kw); idx >= 0 {
						rest = rest[:idx]
					}
				}
				if strings.Count(rest, ",") > 0 {
					result.Issues = append(result.Issues, LintIssue{
						Rule:    findRule("L004"),
						Message: "Implicit cross join (comma in FROM) — consider explicit JOIN syntax",
						SQL:     truncateSQL(stmt, 80),
					})
				}
			}
		}

		// L005: ORDER BY with numeric ordinal
		if strings.Contains(upper, "ORDER BY") {
			orderIdx := strings.Index(upper, "ORDER BY")
			rest := upper[orderIdx+8:]
			re := regexp.MustCompile(`\b\d+\b`)
			if re.MatchString(rest) {
				result.Issues = append(result.Issues, LintIssue{
					Rule:    findRule("L005"),
					Message: "ORDER BY with numeric ordinal — use column names for readability",
					SQL:     truncateSQL(stmt, 80),
				})
			}
		}

		// L006: Deeply nested subqueries
		depth := countNesting(stmt)
		if depth >= 3 {
			result.Issues = append(result.Issues, LintIssue{
				Rule:    findRule("L006"),
				Message: fmt.Sprintf("Deeply nested subqueries (depth %d) — consider using CTEs", depth),
				SQL:     truncateSQL(stmt, 80),
			})
		}

		// L007: Inconsistent keyword case
		tokens := tokenizeSQL(stmt)
		hasUpper, hasLower := false, false
		for _, tok := range tokens {
			if tok.typ == "keyword" {
				w := tok.value
				if w == strings.ToUpper(w) {
					hasUpper = true
				} else if w == strings.ToLower(w) {
					hasLower = true
				}
			}
		}
		if hasUpper && hasLower {
			result.Issues = append(result.Issues, LintIssue{
				Rule:    findRule("L007"),
				Message: "Inconsistent keyword case — use either all UPPERCASE or all lowercase",
				SQL:     truncateSQL(stmt, 80),
			})
		}
	}

	return result
}

func findRule(id string) LintRule {
	for _, r := range lintRules {
		if r.ID == id {
			return r
		}
	}
	return LintRule{ID: id, Name: "unknown", Severity: "info"}
}

func countNesting(sqlStr string) int {
	maxDepth, depth := 0, 0
	inString := false
	for i := 0; i < len(sqlStr); i++ {
		if sqlStr[i] == '\'' {
			if inString {
				// Handle escaped single quotes ('')
				if i+1 < len(sqlStr) && sqlStr[i+1] == '\'' {
					i++ // skip the escaped quote
					continue
				}
				inString = false
			} else {
				inString = true
			}
		}
		if !inString && sqlStr[i] == '(' {
			depth++
			if depth > maxDepth {
				maxDepth = depth
			}
		}
		if !inString && sqlStr[i] == ')' {
			depth--
		}
	}
	return maxDepth
}

func truncateSQL(s string, n int) string {
	s = strings.Join(strings.Fields(s), " ") // collapse whitespace
	if len(s) > n {
		return s[:n-3] + "..."
	}
	return s
}

func splitStatements(sqlStr string) []string {
	var stmts []string
	var buf strings.Builder
	inString := false
	for i := 0; i < len(sqlStr); i++ {
		c := sqlStr[i]
		if c == '\'' {
			inString = !inString
		}
		if c == ';' && !inString {
			s := strings.TrimSpace(buf.String())
			if s != "" {
				stmts = append(stmts, s)
			}
			buf.Reset()
			continue
		}
		buf.WriteByte(c)
	}
	if s := strings.TrimSpace(buf.String()); s != "" {
		stmts = append(stmts, s)
	}
	return stmts
}

// PrintLintResult prints lint findings to stdout.
func PrintLintResult(result LintResult) {
	if len(result.Issues) == 0 {
		fmt.Printf("✓ %d statement(s) checked — no issues found\n", result.Statements)
		return
	}
	errors, warnings, infos := 0, 0, 0
	for _, issue := range result.Issues {
		icon := "ℹ"
		switch issue.Rule.Severity {
		case "error":
			icon = "✗"
			errors++
		case "warning":
			icon = "⚠"
			warnings++
		case "info":
			infos++
		}
		fmt.Printf("%s [%s] %s: %s\n", icon, issue.Rule.ID, issue.Rule.Name, issue.Message)
		if issue.SQL != "" {
			fmt.Printf("  → %s\n", issue.SQL)
		}
	}
	fmt.Printf("\n%d statement(s) checked: %d error(s), %d warning(s), %d info(s)\n",
		result.Statements, errors, warnings, infos)
}

// ============================================================================
// SQL Normalizer
// ============================================================================

// NormalizeSQL converts a SQL statement to a canonical form for comparison/caching.
// It uppercases keywords, normalizes whitespace, and optionally replaces literals
// with placeholders.
func NormalizeSQL(sqlStr string, replaceLiterals bool) string {
	tokens := tokenizeSQL(sqlStr)
	tokens = uppercaseKeywords(tokens)

	var parts []string
	for _, tok := range tokens {
		if replaceLiterals {
			if tok.typ == "string" || tok.typ == "number" {
				parts = append(parts, "?")
				continue
			}
		}
		parts = append(parts, tok.value)
	}
	result := normalizeWhitespace(strings.TrimSpace(strings.Join(parts, " ")))
	return result
}

// ============================================================================
// SQL Diff (compare two SQL files)
// ============================================================================

// DiffResult holds the comparison of two SQL files.
type DiffResult struct {
	OnlyInA []string // statements only in file A
	OnlyInB []string // statements only in file B
	Common  int      // identical statements
}

// DiffSQL compares two SQL texts statement by statement (normalized comparison).
func DiffSQL(sqlA, sqlB string) DiffResult {
	stmtsA := splitStatements(sqlA)
	stmtsB := splitStatements(sqlB)

	normA := make(map[string]string) // normalized → original
	normB := make(map[string]string)

	for _, s := range stmtsA {
		normA[NormalizeSQL(s, false)] = s
	}
	for _, s := range stmtsB {
		normB[NormalizeSQL(s, false)] = s
	}

	result := DiffResult{}
	for norm, orig := range normA {
		if _, ok := normB[norm]; ok {
			result.Common++
			delete(normB, norm)
		} else {
			result.OnlyInA = append(result.OnlyInA, orig)
		}
	}
	for _, orig := range normB {
		result.OnlyInB = append(result.OnlyInB, orig)
	}

	sort.Strings(result.OnlyInA)
	sort.Strings(result.OnlyInB)
	return result
}

// PrintDiffResult displays the diff.
func PrintDiffResult(result DiffResult) {
	fmt.Printf("Common statements: %d\n", result.Common)
	if len(result.OnlyInA) > 0 {
		fmt.Printf("\nOnly in file A (%d):\n", len(result.OnlyInA))
		for _, s := range result.OnlyInA {
			fmt.Printf("  - %s\n", truncateSQL(s, 100))
		}
	}
	if len(result.OnlyInB) > 0 {
		fmt.Printf("\nOnly in file B (%d):\n", len(result.OnlyInB))
		for _, s := range result.OnlyInB {
			fmt.Printf("  + %s\n", truncateSQL(s, 100))
		}
	}
	if len(result.OnlyInA) == 0 && len(result.OnlyInB) == 0 {
		fmt.Println("No differences found.")
	}
}

// readSQLInput reads SQL from args, @filename, or stdin when the sole input is "-".
func readSQLInput(args []string) string {
	sql, err := readSQLInputFrom(args, os.Stdin)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error reading SQL: %v\n", err)
		return ""
	}
	return sql
}

func readSQLInputFrom(args []string, stdin io.Reader) (string, error) {
	if len(args) == 0 {
		return "", nil
	}
	first := args[0]
	if first == "-" && len(args) == 1 {
		data, err := io.ReadAll(stdin)
		if err != nil {
			return "", err
		}
		return strings.TrimSpace(string(data)), nil
	}
	if strings.HasPrefix(first, "@") {
		data, err := os.ReadFile(first[1:])
		if err != nil {
			return "", err
		}
		return strings.TrimSpace(string(data)), nil
	}
	return strings.Join(args, " "), nil
}

// ============================================================================
// Interactive SQL Tools REPL
// ============================================================================

func main() {
	beautifyCmd := flag.NewFlagSet("beautify", flag.ExitOnError)
	beautifyUpper := beautifyCmd.Bool("upper", true, "Uppercase keywords")

	validateCmd := flag.NewFlagSet("validate", flag.ExitOnError)

	explainCmd := flag.NewFlagSet("explain", flag.ExitOnError)

	lintCmd := flag.NewFlagSet("lint", flag.ExitOnError)

	normalizeCmd := flag.NewFlagSet("normalize", flag.ExitOnError)
	normPlaceholders := normalizeCmd.Bool("placeholders", false, "Replace literals with ? placeholders")

	diffCmd := flag.NewFlagSet("diff", flag.ExitOnError)

	replCmd := flag.NewFlagSet("repl", flag.ExitOnError)
	replTenant := replCmd.String("tenant", "default", "Tenant name")

	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}

	switch os.Args[1] {
	case "beautify":
		beautifyCmd.Parse(os.Args[2:])
		sql := readSQLInput(beautifyCmd.Args())
		if sql == "" {
			fmt.Println("Usage: sqltools beautify [-upper=true] <sql>  or  sqltools beautify @file.sql")
			os.Exit(1)
		}
		opts := DefaultBeautifyOptions()
		opts.Uppercase = *beautifyUpper
		b := NewSQLBeautifier(opts)
		fmt.Println(b.Beautify(sql))

	case "validate":
		validateCmd.Parse(os.Args[2:])
		sql := readSQLInput(validateCmd.Args())
		if sql == "" {
			fmt.Println("Usage: sqltools validate <sql>  or  sqltools validate @file.sql")
			os.Exit(1)
		}
		result := ValidateSQL(sql)
		if result.Valid {
			fmt.Printf("✓ Valid %s statement\n", result.SQLType)
			for _, w := range result.Warnings {
				fmt.Printf("⚠ Warning: %s\n", w)
			}
		} else {
			fmt.Printf("✗ Invalid SQL: %s\n", result.Error)
			os.Exit(1)
		}

	case "explain":
		explainCmd.Parse(os.Args[2:])
		sql := readSQLInput(explainCmd.Args())
		if sql == "" {
			fmt.Println("Usage: sqltools explain <sql>  or  sqltools explain @file.sql")
			os.Exit(1)
		}
		plan, err := ExplainQuery(sql)
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			os.Exit(1)
		}
		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
		PrintPlan(plan, w)

	case "lint":
		lintCmd.Parse(os.Args[2:])
		sql := readSQLInput(lintCmd.Args())
		if sql == "" {
			fmt.Println("Usage: sqltools lint <sql>  or  sqltools lint @file.sql")
			os.Exit(1)
		}
		result := LintSQL(sql)
		PrintLintResult(result)
		if len(result.Issues) > 0 {
			for _, issue := range result.Issues {
				if issue.Rule.Severity == "error" {
					os.Exit(1)
				}
			}
		}

	case "normalize":
		normalizeCmd.Parse(os.Args[2:])
		sql := readSQLInput(normalizeCmd.Args())
		if sql == "" {
			fmt.Println("Usage: sqltools normalize [-placeholders] <sql>  or  sqltools normalize @file.sql")
			os.Exit(1)
		}
		fmt.Println(NormalizeSQL(sql, *normPlaceholders))

	case "diff":
		diffCmd.Parse(os.Args[2:])
		args := diffCmd.Args()
		if len(args) < 2 {
			fmt.Println("Usage: sqltools diff <fileA.sql> <fileB.sql>")
			os.Exit(1)
		}
		dataA, err := os.ReadFile(args[0])
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", args[0], err)
			os.Exit(1)
		}
		dataB, err := os.ReadFile(args[1])
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", args[1], err)
			os.Exit(1)
		}
		result := DiffSQL(string(dataA), string(dataB))
		PrintDiffResult(result)

	case "templates":
		fmt.Println("Available Query Templates:")
		fmt.Println("==========================")
		for _, t := range CommonTemplates() {
			fmt.Printf("\n%s - %s\n", t.Name, t.Description)
			fmt.Printf("  SQL: %s\n", t.SQL)
			fmt.Printf("  Parameters: %s\n", strings.Join(t.Parameters, ", "))
		}

	case "repl":
		replCmd.Parse(os.Args[2:])
		runToolsREPL(*replTenant)

	default:
		printUsage()
		os.Exit(1)
	}
}

func printUsage() {
	fmt.Println(`SQL Tools - SSMS-like features for tinySQL

Commands:
  beautify [-upper=true] <sql>    Format SQL statement
  validate <sql>                  Check SQL syntax
  explain <sql>                   Show query execution plan
  lint <sql>                      Multi-rule SQL analysis
  normalize [-placeholders] <sql> Canonicalize SQL for comparison
  diff <fileA.sql> <fileB.sql>    Compare two SQL files
  templates                       List query templates
  repl [-tenant=default]          Interactive SQL tools shell

File input: Use @filename to read SQL from a file, e.g.:
  sqltools beautify @query.sql
  sqltools lint @migrations.sql
Standard input: Use - to read SQL from a pipe, e.g.:
  printf 'SELECT 1' | sqltools validate -

Examples:
  sqltools beautify "select * from users where id=1"
  sqltools validate "SELECT name FROM users"
  sqltools lint "DELETE FROM users; SELECT * FROM orders"
  sqltools normalize -placeholders "SELECT * FROM users WHERE id = 42"
  sqltools diff old_schema.sql new_schema.sql
  sqltools explain "SELECT * FROM orders JOIN users ON orders.user_id = users.id"
  sqltools repl`)
}

func runToolsREPL(tenant string) {
	db := tsql.NewDB()
	ctx := context.Background()
	history := NewQueryHistory(100)
	beautifier := NewSQLBeautifier(DefaultBeautifyOptions())
	browser := NewSchemaBrowser(db)

	fmt.Println("SQL Tools REPL")
	fmt.Println("Type .help for commands, .quit to exit")
	fmt.Println()

	scanner := bufio.NewScanner(os.Stdin)
	var sqlBuf strings.Builder

	for {
		if sqlBuf.Len() == 0 {
			fmt.Print("sql> ")
		} else {
			fmt.Print(" ... ")
		}

		if !scanner.Scan() {
			break
		}

		line := strings.TrimSpace(scanner.Text())

		if sqlBuf.Len() == 0 && strings.HasPrefix(line, ".") {
			handleToolsCommand(line, db, tenant, history, beautifier, browser)
			continue
		}

		sqlBuf.WriteString(line)

		if strings.HasSuffix(line, ";") {
			sql := strings.TrimSuffix(strings.TrimSpace(sqlBuf.String()), ";")
			sqlBuf.Reset()

			if sql == "" {
				continue
			}

			start := time.Now()
			stmt, err := tsql.ParseSQL(sql)
			if err != nil {
				history.Add(sql, time.Since(start), 0, err)
				fmt.Printf("Parse error: %v\n", err)
				continue
			}

			rs, err := tsql.Execute(ctx, db, tenant, stmt)
			duration := time.Since(start)

			if err != nil {
				history.Add(sql, duration, 0, err)
				fmt.Printf("Error: %v\n", err)
				continue
			}

			history.Add(sql, duration, len(rs.Rows), nil)

			if len(rs.Rows) > 0 {
				w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
				fmt.Fprintln(w, strings.Join(rs.Cols, "\t"))
				for _, row := range rs.Rows {
					var vals []string
					for _, col := range rs.Cols {
						v, _ := tsql.GetVal(row, col)
						if v == nil {
							vals = append(vals, "NULL")
						} else {
							vals = append(vals, fmt.Sprintf("%v", v))
						}
					}
					fmt.Fprintln(w, strings.Join(vals, "\t"))
				}
				w.Flush()
			}
			fmt.Printf("(%d rows, %v)\n", len(rs.Rows), duration)
		} else {
			sqlBuf.WriteString(" ")
		}
	}
}

// toolsHandleSchema prints schema for one or all tables.
func toolsHandleSchema(parts []string, tenant string, browser *SchemaBrowser) {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	if len(parts) > 1 {
		info, err := browser.DescribeTable(tenant, parts[1])
		if err != nil {
			fmt.Println("Error:", err)
			return
		}
		fmt.Printf("Table: %s (%d rows)\n", info.Name, info.RowCount)
		fmt.Fprintf(w, "Column\tType\tFlags\n")
		fmt.Fprintf(w, "------\t----\t-----\n")
		for _, col := range info.Columns {
			flags := ""
			if col.Primary {
				flags += "PK "
			}
			if !col.Nullable {
				flags += "NOT NULL"
			}
			fmt.Fprintf(w, "%s\t%s\t%s\n", col.Name, col.Type, flags)
		}
		w.Flush()
	} else {
		browser.PrintSchema(tenant, w)
	}
}

// toolsHandleValidate validates SQL syntax and prints the result.
func toolsHandleValidate(parts []string) {
	if len(parts) < 2 {
		fmt.Println("Usage: .validate <sql>")
		return
	}
	sql := strings.Join(parts[1:], " ")
	result := ValidateSQL(sql)
	if result.Valid {
		fmt.Printf("✓ Valid %s statement\n", result.SQLType)
		for _, w := range result.Warnings {
			fmt.Printf("⚠ %s\n", w)
		}
	} else {
		fmt.Printf("✗ Invalid: %s\n", result.Error)
	}
}

// toolsHandleExplain prints the query plan for a SQL statement.
func toolsHandleExplain(parts []string) {
	if len(parts) < 2 {
		fmt.Println("Usage: .explain <sql>")
		return
	}
	sql := strings.Join(parts[1:], " ")
	plan, err := ExplainQuery(sql)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	PrintPlan(plan, w)
}

// toolsHandleTemplate prints details of a named template.
func toolsHandleTemplate(parts []string) {
	if len(parts) < 2 {
		fmt.Println("Usage: .template <name>")
		return
	}
	name := parts[1]
	for _, t := range CommonTemplates() {
		if t.Name == name {
			fmt.Printf("Name: %s\n", t.Name)
			fmt.Printf("Description: %s\n", t.Description)
			fmt.Printf("SQL: %s\n", t.SQL)
			fmt.Printf("Parameters: %s\n", strings.Join(t.Parameters, ", "))
			return
		}
	}
	fmt.Printf("Template %q not found\n", name)
}

func handleToolsCommand(cmd string, db *tsql.DB, tenant string, history *QueryHistory, beautifier *SQLBeautifier, browser *SchemaBrowser) {
	parts := strings.Fields(cmd)
	if len(parts) == 0 {
		return
	}

	switch parts[0] {
	case ".help":
		fmt.Println(`
Commands:
  .help                 Show this help
  .quit                 Exit
  .tables               List tables
  .schema [table]       Show schema (all tables or specific table)
  .history [n]          Show last n queries (default 10)
  .beautify <sql>       Format SQL
  .validate <sql>       Check SQL syntax
  .explain <sql>        Show query plan
  .export <format> <file> <sql>   Export results (csv, json, ndjson, sql)
  .template <name>      Show template
  .templates            List all templates`)

	case ".quit", ".exit":
		os.Exit(0)

	case ".tables":
		tables := browser.ListTables(tenant)
		for _, t := range tables {
			fmt.Println(t)
		}

	case ".schema":
		toolsHandleSchema(parts, tenant, browser)

	case ".history":
		n := 10
		if len(parts) > 1 {
			fmt.Sscanf(parts[1], "%d", &n)
		}
		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
		history.PrintHistory(w, history.Last(n))

	case ".beautify":
		if len(parts) < 2 {
			fmt.Println("Usage: .beautify <sql>")
			return
		}
		sql := strings.Join(parts[1:], " ")
		fmt.Println(beautifier.Beautify(sql))

	case ".validate":
		toolsHandleValidate(parts)

	case ".explain":
		toolsHandleExplain(parts)

	case ".templates":
		for _, t := range CommonTemplates() {
			fmt.Printf("  %s - %s\n", t.Name, t.Description)
		}

	case ".template":
		toolsHandleTemplate(parts)

	case ".export":
		toolsHandleExport(parts, db, tenant)

	default:
		fmt.Printf("Unknown command: %s (type .help for commands)\n", parts[0])
	}
}

func toolsHandleExport(parts []string, db *tsql.DB, tenant string) {
	if len(parts) < 4 {
		fmt.Println("Usage: .export <csv|json|ndjson|sql> <file> <sql>")
		return
	}
	stmt, err := tsql.ParseSQL(strings.Join(parts[3:], " "))
	if err != nil {
		fmt.Println("Parse error:", err)
		return
	}
	rs, err := tsql.Execute(context.Background(), db, tenant, stmt)
	if err != nil {
		fmt.Println("Query error:", err)
		return
	}
	f, err := os.Create(parts[2])
	if err != nil {
		fmt.Println("Create export:", err)
		return
	}
	defer f.Close()
	if err := NewExporter(ExportFormat(strings.ToLower(parts[1]))).Export(rs, "result", f); err != nil {
		fmt.Println("Export error:", err)
		return
	}
	fmt.Printf("Exported %d row(s) to %s\n", len(rs.Rows), parts[2])
}
