package db

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/Gleipnir-Technology/bob"
	"github.com/Gleipnir-Technology/bob/dialect/psql"
	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
	"github.com/stephenafamo/scan"
)

func TestPreparedQueryOld(ctx context.Context) error {
	type Skn struct {
		Result int
	}
	q := fmt.Sprintf("EXECUTE test_function(%d)", 4)
	query := psql.RawQuery(q)
	result, err := bob.One[Skn](ctx, PGInstance.BobDB, query, scan.StructMapper[Skn]())
	if err != nil {
		return fmt.Errorf("Failed to exectue test function: %w", err)
	}
	log.Info().Int("value", result.Result).Msg("got result")

	return nil
}

// SqlParam is a generic struct that wraps a parameter with its SQL representation
type SqlParam interface {
	ToSql() string
}

type JsonBParam struct {
	Name  string
	Value json.RawMessage
}

func (p JsonBParam) ToSql() string {
	if len(p.Value) == 0 {
		return fmt.Sprintf("%s => '{}'::jsonb", p.Name)
	}
	return fmt.Sprintf("%s => '%s'::jsonb", p.Name, p.Value)
}

type GeometryLine struct {
	Paths [][][]float64 `json:"paths"`
}
type GISLineParam struct {
	Name  string
	Value GeometryLine
	WKID  int
}

func (p GISLineParam) ToSql() string {
	pairs := make([]string, 0)
	if len(p.Value.Paths) > 1 {
		log.Warn().Msg("Looks like we need to implement multi-path lines")
	}
	for _, path := range p.Value.Paths {
		for _, pair := range path {
			if len(pair) != 2 {
				log.Warn().Int("len", len(pair)).Msg("Too many coords in line point")
				continue
			}
			pairs = append(pairs, fmt.Sprintf("%f %f", pair[0], pair[1]))
		}
	}
	var b strings.Builder
	for i, p := range pairs {
		b.WriteString(p)
		if i < len(pairs)-1 {
			b.WriteString(", ")
		}
	}
	// Not sure why this isn't working, postgres is complaining that "function x is not uinque" and that
	// I may want to add explicit casts...
	//return fmt.Sprintf("%s => ST_SetSRID('LINESTRING(%s)', %d)::geometry(LineString,%d)", p.Name, b.String(), p.WKID, p.WKID)
	return fmt.Sprintf("%s => 'LINESTRING(%s)'::geometry", p.Name, b.String())
}

type GeometryPoint struct {
	X float64 `json:"x"`
	Y float64 `json:"y"`
}
type GISPointParam struct {
	Name  string
	Value GeometryPoint
	WKID  int
}

func (p GISPointParam) ToSql() string {
	return fmt.Sprintf("%s => ST_SetSRID(ST_MakePoint(%f, %f), %d)", p.Name, p.Value.X, p.Value.Y, p.WKID)
}

type GeometryPolygon struct {
	Rings [][][]float64 `json:"rings"`
}
type GISPolygonParam struct {
	Name  string
	Value GeometryPolygon
	WKID  int
}

func (p GISPolygonParam) ToSql() string {
	pairs := make([]string, 0)
	if len(p.Value.Rings) > 1 {
		log.Warn().Msg("Looks like we need to implement multi-path lines")
	}
	for _, path := range p.Value.Rings {
		for _, pair := range path {
			if len(pair) != 2 {
				log.Warn().Int("len", len(pair)).Msg("Too many coords in line point")
				continue
			}
			pairs = append(pairs, fmt.Sprintf("%f %f", pair[0], pair[1]))
		}
	}
	var b strings.Builder
	for i, p := range pairs {
		b.WriteString(p)
		if i < len(pairs)-1 {
			b.WriteString(", ")
		}
	}
	// Not sure why this isn't working, postgres is complaining that "function x is not uinque" and that
	// I may want to add explicit casts...
	//return fmt.Sprintf("%s => ST_SetSRID('LINESTRING(%s)', %d)::geometry(LineString,%d)", p.Name, b.String(), p.WKID, p.WKID)
	return fmt.Sprintf("%s => ST_MakePolygon('LINESTRING(%s)')::geometry", p.Name, b.String())
}

// StringParam wraps a string parameter
type StringParam struct {
	Name  string
	Value string
}

func (p StringParam) ToSql() string {
	// Escape quotes since we are writing text directly into the SQL query and this is a key delimiter
	escapedQuotes := strings.ReplaceAll(string(p.Value), "'", "''")
	// Escape question marks because they are a special signal for replacement to bob
	escapedQuestions := strings.ReplaceAll(escapedQuotes, "?", "\\?")
	return fmt.Sprintf("%s => '%s'::varchar", p.Name, escapedQuestions)
}

type Float64Param struct {
	Name  string
	Value float64
}

func (p Float64Param) ToSql() string {
	return fmt.Sprintf("%s => %f::double precision", p.Name, p.Value)
}

// IntParam wraps an int parameter
type Int16Param struct {
	Name  string
	Value int16
}

func (p Int16Param) ToSql() string {
	return fmt.Sprintf("%s => %d::smallint", p.Name, p.Value)
}

type Int32Param struct {
	Name  string
	Value int32
}

func (p Int32Param) ToSql() string {
	return fmt.Sprintf("%s => %d::int", p.Name, p.Value)
}

type Int64Param struct {
	Name  string
	Value int64
}

func (p Int64Param) ToSql() string {
	return fmt.Sprintf("%s => %d::bigint", p.Name, p.Value)
}

// UintParam wraps a uint parameter
type UintParam struct {
	Name  string
	Value uint
}

func (p UintParam) ToSql() string {
	return fmt.Sprintf("%s => %d::int", p.Name, p.Value)
}

type Uint32Param struct {
	Name  string
	Value uint32
}

func (p Uint32Param) ToSql() string {
	return fmt.Sprintf("%s => %d::int", p.Name, p.Value)
}

type Uint64Param struct {
	Name  string
	Value uint64
}

func (p Uint64Param) ToSql() string {
	return fmt.Sprintf("%s => %d::bigint", p.Name, p.Value)
}

type UUIDParam struct {
	Name  string
	Value string
}

func (p UUIDParam) ToSql() string {
	return fmt.Sprintf("%s => '%s'", p.Name, p.Value)
}

// FloatParam wraps a float parameter
type FloatParam float64

func (p FloatParam) ToSql() string {
	return fmt.Sprintf("%f", p)
}

// BoolParam wraps a boolean parameter
type BoolParam bool

func (p BoolParam) ToSql() string {
	return fmt.Sprintf("%t", p)
}

// EnumParam wraps a PostgreSQL enum parameter
type EnumParam string

func (p EnumParam) ToSql() string {
	escapedStr := strings.ReplaceAll(string(p), "'", "''")
	return fmt.Sprintf("'%s'", escapedStr)
}

// NullParam represents a NULL value
type NullParam struct {
	Name string
}

func (p NullParam) ToSql() string {
	return fmt.Sprintf("%s => NULL", p.Name)
}

// Convenience functions to create typed parameters
func String(n, s string) StringParam {
	return StringParam{
		Name:  n,
		Value: s,
	}
}

type Stringable interface {
	String() string
}

func Enum(n string, e Stringable) EnumParam {
	return EnumParam(e.String())
}
func Float64(n string, f float64) Float64Param {
	return Float64Param{n, f}
}
func GISLine(n string, v GeometryLine, wkid int) GISLineParam {
	return GISLineParam{n, v, wkid}
}
func GISPoint(n string, v GeometryPoint, wkid int) GISPointParam {
	return GISPointParam{n, v, wkid}
}
func GISPolygon(n string, v GeometryPolygon, wkid int) GISPolygonParam {
	return GISPolygonParam{n, v, wkid}
}
func Int16(n string, i int16) Int16Param {
	return Int16Param{n, i}
}
func Int32(n string, i int32) Int32Param {
	return Int32Param{n, i}
}
func Int64(n string, i int64) Int64Param {
	return Int64Param{n, i}
}
func JsonB(n string, v json.RawMessage) JsonBParam {
	return JsonBParam{n, v}
}

// Timestamp creates a PostgreSQL TIMESTAMP WITHOUT TIME ZONE parameter
func Timestamp(name string, t time.Time) TimestampParam {
	return TimestampParam{name, t}
}

// Timestamptz creates a PostgreSQL TIMESTAMP WITH TIME ZONE parameter
func Timestamptz(t time.Time) TimestamptzParam {
	return TimestamptzParam(t)
}

func Uint(name string, u uint) UintParam {
	return UintParam{name, u}
}
func Uint32(name string, u uint) Uint32Param {
	return Uint32Param{name, uint32(u)}
}
func Uint64(name string, u uint64) Uint64Param {
	return Uint64Param{name, u}
}
func UUID(name string, u uuid.UUID) UUIDParam {
	return UUIDParam{name, u.String()}
}

func Float(f float64) FloatParam {
	return FloatParam(f)
}

func Bool(b bool) BoolParam {
	return BoolParam(b)
}

func Null() NullParam {
	return NullParam{}
}

// TimestampParam wraps a time.Time parameter for PostgreSQL TIMESTAMP WITHOUT TIME ZONE
type TimestampParam struct {
	Name  string
	Value time.Time
}

func (p TimestampParam) ToSql() string {
	// Format as PostgreSQL timestamp without timezone
	// The format string is based on PostgreSQL's expected format
	t := time.Time(p.Value)
	return fmt.Sprintf("%s => '%s'::timestamp", p.Name, t.Format("2006-01-02 15:04:05.999999"))
}

// TimestamptzParam wraps a time.Time parameter for PostgreSQL TIMESTAMP WITH TIME ZONE
type TimestamptzParam time.Time

func (p TimestamptzParam) ToSql() string {
	// Format as PostgreSQL timestamp with timezone
	t := time.Time(p)
	return fmt.Sprintf("'%s'::timestamptz", t.Format("2006-01-02 15:04:05.999999-07:00"))
}
