// KEKS -- Go KEKS codec implementation
// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, version 3 of the License.
//
// 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program.  If not, see <http://www.gnu.org/licenses/>.

package schema

import (
	"bytes"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	"go.cypherpunks.su/tai64n/v4"

	"go.cypherpunks.su/keks"
	"go.cypherpunks.su/keks/be"
	"go.cypherpunks.su/keks/types"
)

const (
	CmdEach      = "*"
	CmdEq        = "="
	CmdExists    = "E"
	CmdGT        = ">"
	CmdLT        = "<"
	CmdNotExists = "!E"
	CmdSchema    = "S"
	CmdTake      = "."
	CmdTimePrec  = "TP"
	CmdType      = "T"
	CmdUTC       = "UTC"
	Magic        = "schema"
)

type BaseErr struct {
	Data       any
	w          error
	SchemaName string
	CmdName    string
	Taken      string
	Msg        string
	CmdIdx     int
}

type SchemaErr struct{ BaseErr }

type DataErr struct{ BaseErr }

func (err *BaseErr) Error() string {
	cols := []string{
		fmt.Sprintf("schema:%s", err.SchemaName),
		fmt.Sprintf("cmd:%d:%s", err.CmdIdx, err.CmdName),
	}
	if err.Taken != "" {
		cols = append(cols, fmt.Sprintf("taken:%s", err.Taken))
	}
	if err.Data != nil {
		cols = append(cols, fmt.Sprintf("data:%T:%+v", err.Data, err.Data))
	}
	cols = append(cols, err.Msg)
	return strings.Join(cols, " ")
}

func (err *BaseErr) Unwrap() error {
	return err.w
}

func Check(schemaName string, schemas map[string][][]any, data any) error {
	cmds := schemas[schemaName]
	if cmds == nil {
		return &SchemaErr{BaseErr: BaseErr{
			SchemaName: schemaName, Msg: "no such schema",
		}}
	}
	var taken string
	vs := []any{data}
	for cmdIdx, cmd := range cmds {
		if len(cmd) == 0 {
			return &SchemaErr{BaseErr: BaseErr{
				SchemaName: schemaName,
				CmdIdx:     cmdIdx,
				Msg:        "empty command",
			}}
		}
		cmdName, ok := cmd[0].(string)
		if !ok {
			return &SchemaErr{BaseErr: BaseErr{
				SchemaName: schemaName,
				CmdIdx:     cmdIdx,
				Msg:        "non str command",
			}}
		}
		switch cmdName {
		case CmdExists:
			if vs == nil {
				return &DataErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Taken:      taken,
				}}
			}
		case CmdNotExists:
			if vs != nil {
				return &DataErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Taken:      taken,
				}}
			}
		case CmdTake:
			taken = ""
			vs = nil
			if len(cmd) != 2 {
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "wrong number of args",
				}}
			}
			switch k := cmd[1].(type) {
			case nil:
				taken = "."
				vs = []any{data}
			case string:
				taken = ":" + k
				var m map[string]any
				m, ok = data.(map[string]any)
				if !ok {
					return &SchemaErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Msg:        "non map",
						Data:       data,
					}}
				}
				v, exists := m[k]
				if !exists {
					continue
				}
				vs = []any{v}
			case uint64:
				taken = strconv.FormatUint(k, 10)
				var l []any
				l, ok = data.([]any)
				if !ok {
					return &SchemaErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Msg:        "non list",
						Data:       data,
					}}
				}
				if int(k) < len(l) {
					vs = []any{l[k]}
				}
			default:
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "bad target",
				}}
			}
		case CmdEach:
			if vs == nil {
				continue
			}
			v := vs[0]
			switch v := v.(type) {
			case map[string]any:
				vs = vs[:0]
				for _, v := range v {
					vs = append(vs, v)
				}
			case []any:
				vs = v
			default:
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Taken:      taken,
					Msg:        "non iterable",
				}}
			}
			taken += "*"
		case CmdEq:
			if vs == nil {
				continue
			}
			if len(cmd) != 2 {
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "wrong number of args",
				}}
			}
			var expect []byte
			expect, ok = cmd[1].([]byte)
			if !ok {
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "bad bin",
					Data:       cmd[1],
				}}
			}
			for _, v := range vs {
				var eq bool
				switch got := v.(type) {
				case string:
					eq = got == string(expect)
				case []byte:
					eq = bytes.Equal(got, expect)
				case *keks.Hexlet:
					eq = bytes.Equal(got[:], expect)
				case *tai64n.TAI64:
					eq = bytes.Equal(got[:], expect)
				case *tai64n.TAI64N:
					eq = bytes.Equal(got[:], expect)
				case *tai64n.TAI64NA:
					eq = bytes.Equal(got[:], expect)
				case keks.Magic:
					eq = string(got) == string(expect)
				case keks.Raw:
					eq = bytes.Equal(got, expect)
				default:
					if !ok {
						return &SchemaErr{BaseErr: BaseErr{
							SchemaName: schemaName,
							CmdIdx:     cmdIdx,
							CmdName:    cmdName,
							Taken:      taken,
							Msg:        "non comparable",
							Data:       v,
						}}
					}
				}
				if !eq {
					return &DataErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Taken:      taken,
						Data:       v,
					}}
				}
			}
		case CmdType:
			if vs == nil {
				continue
			}
			if len(cmd) < 2 {
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "wrong number of args",
				}}
			}
			expected := make([]types.Type, 0, len(cmd)-1)
			for _, tAny := range cmd[1:] {
				var t string
				t, ok = tAny.(string)
				if !ok {
					return &SchemaErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Msg:        "non str",
						Data:       tAny,
					}}
				}
				switch t {
				case "N":
					expected = append(expected, types.NIL)
				case "b":
					expected = append(expected, types.Bool)
				case "H":
					expected = append(expected, types.Hexlet)
				case "I":
					expected = append(expected, types.UInt, types.Int)
				case "L":
					expected = append(expected, types.List)
				case "M":
					expected = append(expected, types.Map)
				case "O":
					expected = append(expected, types.Blob)
				case "T":
					expected = append(expected, types.TAI64, types.TAI64N, types.TAI64NA)
				case "K":
					expected = append(expected, types.Magic)
				case "B":
					expected = append(expected, types.Bin)
				case "S":
					expected = append(expected, types.Str)
				default:
					return &SchemaErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Msg:        "unknown type",
						Data:       t,
					}}
				}
			}
			var typ types.Type
			for _, v := range vs {
				switch v.(type) {
				case nil:
					typ = types.NIL
				case bool:
					typ = types.Bool
				case *keks.Hexlet:
					typ = types.Hexlet
				case uint64:
					typ = types.UInt
				case int64:
					typ = types.Int
				case []any:
					typ = types.List
				case map[string]any:
					typ = types.Map
				case *keks.BlobChunked:
					typ = types.Blob
				case *tai64n.TAI64:
					typ = types.TAI64
				case *tai64n.TAI64N:
					typ = types.TAI64N
				case *tai64n.TAI64NA:
					typ = types.TAI64NA
				case time.Time:
					typ = types.TAI64N
				case keks.Magic:
					typ = types.Magic
				case []byte:
					typ = types.Bin
				case string:
					typ = types.Str
				case keks.Raw:
					typ = types.Raw
				default:
					return &SchemaErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Taken:      taken,
						Msg:        "unsupported type",
						Data:       v,
					}}
				}
				if !slices.Contains(expected, typ) {
					return &DataErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Taken:      taken,
						Data:       v,
					}}
				}
			}
		case CmdGT, CmdLT:
			if vs == nil {
				continue
			}
			if len(cmd) != 2 {
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "wrong number of args",
				}}
			}
			var expect int64
			switch v := cmd[1].(type) {
			case uint64:
				expect = int64(v)
			case int64:
				expect = v
			default:
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "unsupported type",
					Data:       v,
				}}
			}
			for _, v := range vs {
				var got int64
				switch v := v.(type) {
				case string:
					got = int64(len(v))
				case []byte:
					got = int64(len(v))
				case []any:
					got = int64(len(v))
				case map[string]any:
					got = int64(len(v))
				case uint64:
					got = int64(v)
				case int64:
					got = v
				default:
					return &SchemaErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Taken:      taken,
						Msg:        "non len-able",
						Data:       v,
					}}
				}
				ok = false
				switch cmdName {
				case CmdGT:
					if got > expect {
						ok = true
					}
				case CmdLT:
					if got < expect {
						ok = true
					}
				}
				if !ok {
					return &DataErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Taken:      taken,
						Data:       got,
					}}
				}
			}
		case CmdSchema:
			if vs == nil {
				continue
			}
			if len(cmd) != 2 {
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "wrong number of args",
				}}
			}
			name, ok := cmd[1].(string)
			if !ok {
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "non str",
					Data:       name,
				}}
			}
			for _, v := range vs {
				if err := Check(name, schemas, v); err != nil {
					return &DataErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Taken:      taken,
						Data:       v,
						w:          err,
					}}
				}
			}
		case CmdTimePrec:
			if vs == nil {
				continue
			}
			if len(cmd) != 2 {
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "wrong number of args",
				}}
			}
			prec, ok := cmd[1].(uint64)
			if !ok {
				return &SchemaErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Msg:        "bad precision value",
					Data:       cmd[1],
				}}
			}
			for _, v := range vs {
				switch v.(type) {
				case *tai64n.TAI64:
				case *tai64n.TAI64N:
				case *tai64n.TAI64NA:
				case time.Time:
				default:
					return &SchemaErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Taken:      taken,
						Msg:        "unsupported data type",
						Data:       v,
					}}
				}
				err := DataErr{BaseErr: BaseErr{
					SchemaName: schemaName,
					CmdIdx:     cmdIdx,
					CmdName:    cmdName,
					Taken:      taken,
					Data:       v,
				}}
				switch prec {
				case 0:
					err.Msg = "s"
					switch v := v.(type) {
					case *tai64n.TAI64:
					case time.Time:
						if v.Nanosecond() != 0 {
							return &err
						}
					default:
						return &err
					}
				case 3:
					err.Msg = "ms"
					switch v := v.(type) {
					case *tai64n.TAI64:
					case *tai64n.TAI64N:
						d := be.Get(v[8:])
						if d%1000000 != 0 {
							return &err
						}
					case time.Time:
						if v.Nanosecond()%1000000 != 0 {
							return &err
						}
					default:
						return &err
					}
				case 6:
					err.Msg = "µs"
					switch v := v.(type) {
					case *tai64n.TAI64:
					case *tai64n.TAI64N:
						d := be.Get(v[8:])
						if d%1000 != 0 {
							return &err
						}
					case time.Time:
						if v.Nanosecond()%1000 != 0 {
							return &err
						}
					default:
						return &err
					}
				case 9:
					err.Msg = "ns"
					switch v.(type) {
					case *tai64n.TAI64:
					case *tai64n.TAI64N:
					case time.Time:
					default:
						return &err
					}
				case 12:
					err.Msg = "ps"
					switch v := v.(type) {
					case time.Time:
					case *tai64n.TAI64:
					case *tai64n.TAI64N:
					case *tai64n.TAI64NA:
						d := be.Get(v[12:])
						if d%1000000 != 0 {
							return &err
						}
					}
				case 15:
					err.Msg = "fs"
					switch v := v.(type) {
					case time.Time:
					case *tai64n.TAI64:
					case *tai64n.TAI64N:
					case *tai64n.TAI64NA:
						d := be.Get(v[12:])
						if d%1000 != 0 {
							return &err
						}
					}
				default:
					return &SchemaErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Msg:        "unknown value",
						Data:       v,
					}}
				}
			}
		case CmdUTC:
			if vs == nil {
				continue
			}
			for _, v := range vs {
				var isLeap bool
				switch v := v.(type) {
				case *tai64n.TAI64:
					_, isLeap = tai64n.Leapsecs.Sub(v.Time())
				case *tai64n.TAI64N:
					_, isLeap = tai64n.Leapsecs.Sub(v.Time())
				case *tai64n.TAI64NA:
					var t tai64n.TAI64
					copy(t[:], v[:])
					_, isLeap = tai64n.Leapsecs.Sub(t.Time())
				case time.Time:
					continue
				default:
					return &SchemaErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Taken:      taken,
						Msg:        "unsupported data type",
						Data:       v,
					}}
				}
				if isLeap {
					return &DataErr{BaseErr: BaseErr{
						SchemaName: schemaName,
						CmdIdx:     cmdIdx,
						CmdName:    cmdName,
						Taken:      taken,
						Data:       v,
					}}
				}
			}
		default:
			return &SchemaErr{BaseErr: BaseErr{
				SchemaName: schemaName,
				CmdIdx:     cmdIdx,
				CmdName:    cmdName,
				Msg:        "unknown command",
			}}
		}
	}
	return nil
}
