package lint

import (
	"context"
	_ "embed"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/go-errors/errors"
	"github.com/jackc/pgconn"
	"github.com/jackc/pgx/v4"
	"github.com/spf13/afero"
	"github.com/supabase/cli/internal/utils"
	"github.com/supabase/cli/pkg/migration"
)

const ENABLE_PGSQL_CHECK = "CREATE EXTENSION IF NOT EXISTS plpgsql_check"

var (
	AllowedLevels = []string{
		"warning",
		"error",
	}
	//go:embed templates/check.sql
	checkSchemaScript string
)

type LintLevel int

func toEnum(level string) LintLevel {
	for i, curr := range AllowedLevels {
		if strings.HasPrefix(level, curr) {
			return LintLevel(i)
		}
	}
	return -1
}

func Run(ctx context.Context, schema []string, level string, failOn string, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
	// Sanity checks.
	conn, err := utils.ConnectByConfig(ctx, config, options...)
	if err != nil {
		return err
	}
	defer conn.Close(context.Background())
	// Run lint script
	result, err := LintDatabase(ctx, conn, schema)
	if err != nil {
		return err
	}
	if len(result) == 0 {
		fmt.Fprintln(os.Stderr, "\nNo schema errors found")
		return nil
	}

	// Apply filtering based on the minimum level
	minLevel := toEnum(level)
	filtered := filterResult(result, minLevel)
	err = printResultJSON(filtered, os.Stdout)
	if err != nil {
		return err
	}
	// Check for fail-on condition
	failOnLevel := toEnum(failOn)
	if failOnLevel != -1 {
		for _, r := range filtered {
			for _, issue := range r.Issues {
				if toEnum(issue.Level) >= failOnLevel {
					return fmt.Errorf("fail-on is set to %s, non-zero exit", AllowedLevels[failOnLevel])
				}
			}
		}
	}
	return nil
}

func filterResult(result []Result, minLevel LintLevel) (filtered []Result) {
	for _, r := range result {
		out := Result{Function: r.Function}
		for _, issue := range r.Issues {
			if toEnum(issue.Level) >= minLevel {
				out.Issues = append(out.Issues, issue)
			}
		}
		if len(out.Issues) > 0 {
			filtered = append(filtered, out)
		}
	}
	return filtered
}

func printResultJSON(result []Result, stdout io.Writer) error {
	if len(result) == 0 {
		return nil
	}
	// Pretty print output
	enc := json.NewEncoder(stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(result); err != nil {
		return errors.Errorf("failed to print result json: %w", err)
	}
	return nil
}

func LintDatabase(ctx context.Context, conn *pgx.Conn, schema []string) ([]Result, error) {
	tx, err := conn.Begin(ctx)
	if err != nil {
		return nil, errors.Errorf("failed to begin transaction: %w", err)
	}
	if len(schema) == 0 {
		schema, err = migration.ListUserSchemas(ctx, conn)
		if err != nil {
			return nil, err
		}
	}
	// Always rollback since lint should not have side effects
	defer func() {
		if err := tx.Rollback(context.Background()); err != nil {
			fmt.Fprintln(os.Stderr, err)
		}
	}()
	if _, err := conn.Exec(ctx, ENABLE_PGSQL_CHECK); err != nil {
		return nil, errors.Errorf("failed to enable pgsql_check: %w", err)
	}
	// Batch prepares statements
	batch := pgx.Batch{}
	for _, s := range schema {
		batch.Queue(checkSchemaScript, s)
	}
	br := conn.SendBatch(ctx, &batch)
	defer br.Close()
	var result []Result
	for _, s := range schema {
		fmt.Fprintln(os.Stderr, "Linting schema:", s)
		rows, err := br.Query()
		if err != nil {
			return nil, errors.Errorf("failed to query rows: %w", err)
		}
		// Parse result row
		for rows.Next() {
			var name string
			var data []byte
			if err := rows.Scan(&name, &data); err != nil {
				return nil, errors.Errorf("failed to scan rows: %w", err)
			}
			var r Result
			if err := json.Unmarshal(data, &r); err != nil {
				return nil, errors.Errorf("failed to marshal json: %w", err)
			}
			// Update function name
			r.Function = s + "." + name
			result = append(result, r)
		}
		err = rows.Err()
		if err != nil {
			return nil, errors.Errorf("failed to parse rows: %w", err)
		}
	}
	return result, nil
}

type Query struct {
	Position string `json:"position"`
	Text     string `json:"text"`
}

type Statement struct {
	LineNumber string `json:"lineNumber"`
	Text       string `json:"text"`
}

type Issue struct {
	Level     string     `json:"level"`
	Message   string     `json:"message"`
	Statement *Statement `json:"statement,omitempty"`
	Query     *Query     `json:"query,omitempty"`
	Hint      string     `json:"hint,omitempty"`
	Detail    string     `json:"detail,omitempty"`
	Context   string     `json:"context,omitempty"`
	SQLState  string     `json:"sqlState,omitempty"`
}

type Result struct {
	Function string  `json:"function"`
	Issues   []Issue `json:"issues"`
}
