package worker

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"io"
	"sort"
	"strconv"
	"strings"

	"github.com/pingcap-inc/nexuscrm-app-packs/workers/mcp/internal/transport"
	query "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	mutation "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
)

type queryInput struct {
	Object  string        `json:"object"`
	Fields  []string      `json:"fields"`
	Filters []filterInput `json:"filters,omitempty"`
	Limit   uint32        `json:"limit,omitempty"`
	Offset  uint64        `json:"offset,omitempty"`
	OrderBy []query.Sort  `json:"order_by,omitempty"`
}
type filterInput struct {
	Field    string                   `json:"field"`
	Operator query.ComparisonOperator `json:"operator"`
	Value    json.RawMessage          `json:"value"`
}
type mutationInput struct {
	IdempotencyKey string                     `json:"idempotency_key"`
	Operation      mutation.OperationKind     `json:"operation"`
	Object         string                     `json:"object"`
	RecordID       string                     `json:"record_id,omitempty"`
	Fields         map[string]json.RawMessage `json:"fields,omitempty"`
}

const querySchema = `{"type":"object","additionalProperties":false,"required":["object","fields"],"properties":{"object":{"type":"string","minLength":1},"fields":{"type":"array","minItems":1,"items":{"type":"string"}},"filters":{"type":"array","description":"All filters are ANDed. No formula or SOQL strings.","items":{"type":"object","additionalProperties":false,"required":["field","operator","value"],"properties":{"field":{"type":"string"},"operator":{"enum":["EQUAL","NOT_EQUAL","GREATER_THAN","GREATER_OR_EQUAL","LESS_THAN","LESS_OR_EQUAL","CONTAINS","STARTS_WITH","ENDS_WITH","LIKE","NOT_LIKE"]},"value":{"type":["string","number","boolean","null"]}}}},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0},"order_by":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string"},"direction":{"enum":["ASC","DESC"]}}}}}}`
const mutationSchema = `{"type":"object","additionalProperties":false,"required":["idempotency_key","operation","object"],"properties":{"idempotency_key":{"type":"string","minLength":1,"maxLength":128,"description":"Unique key for this intended write; keep the same key and payload for retries and status checks."},"operation":{"enum":["CREATE","UPDATE","DELETE"]},"object":{"type":"string","minLength":1},"record_id":{"type":"string"},"fields":{"type":"object","description":"Fields to write. Omitted fields are unchanged; null explicitly clears a field."}}}`

func toolCatalog() []transport.Tool {
	return []transport.Tool{
		{Name: "crm_query_records", Description: "Read one CRM record page using your current object, field and record permissions. Select fields explicitly; filter by Id to read one record.", InputSchema: json.RawMessage(querySchema)},
		{Name: "crm_query_my_records", Description: "Read a CRM record page owned by the signed-in user. Runtime supplies the current user identity. Additional filters are ANDed.", InputSchema: json.RawMessage(querySchema)},
		{Name: "crm_mutate_record", Description: "Create, update or delete one CRM record under your current permissions. Runtime owns validation and idempotency. A timeout is not proof of rollback.", InputSchema: json.RawMessage(mutationSchema)},
		{Name: "crm_mutation_status", Description: "Reconcile a write using exactly the original operation, fields and idempotency_key. A missing result is not proof that the write never committed.", InputSchema: json.RawMessage(mutationSchema)},
	}
}

func decode(raw json.RawMessage, target any) error {
	if len(raw) == 0 || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
		return errors.New("object required")
	}
	if _, err := mutation.CanonicalSourceJSON(string(raw)); err != nil {
		return err
	}
	d := json.NewDecoder(bytes.NewReader(raw))
	d.DisallowUnknownFields()
	if err := d.Decode(target); err != nil {
		return err
	}
	if err := d.Decode(new(any)); err != io.EOF {
		return errors.New("one object required")
	}
	return nil
}

func (w *Worker) execute(ctx context.Context, name string, raw json.RawMessage) (any, error) {
	bearer, ok := ctx.Value(userBearerKey{}).(string)
	if !ok || bearer == "" {
		return result(map[string]string{"error": "UNAUTHENTICATED"}, true)
	}
	switch name {
	case "crm_query_records", "crm_query_my_records":
		request, err := lowerQuery(ctx, raw, name == "crm_query_my_records")
		if err != nil {
			return result(map[string]string{"error": "INVALID_ARGUMENTS"}, true)
		}
		response, err := w.ports.Query(ctx, bearer, request)
		if err != nil {
			return failureResult(err, false)
		}
		records := make([]map[string]any, 0, len(response.Records))
		for _, record := range response.Records {
			value, err := projectRecord(record)
			if err != nil {
				return nil, err
			}
			records = append(records, value)
		}
		return result(map[string]any{"records": records, "total_size": response.TotalSize, "total_size_unknown": response.TotalSizeUnknown, "done": response.Done}, false)
	case "crm_mutate_record", "crm_mutation_status":
		request, err := lowerMutation(ctx, raw)
		if err != nil {
			return result(map[string]string{"error": "INVALID_ARGUMENTS"}, true)
		}
		if name == "crm_mutation_status" {
			response, err := w.ports.Status(ctx, bearer, request)
			if err != nil {
				return failureResult(err, true)
			}
			results, failed, err := projectMutationResults(response.Results)
			if err != nil {
				return nil, err
			}
			var state string
			switch response.State {
			case mutation.CommandAccepted:
				state = "ACCEPTED"
			case mutation.CommandExecuting:
				state = "EXECUTING"
			case mutation.CommandCompleted:
				state = "COMPLETED"
			default:
				return nil, errors.New("unexpected Runtime command state")
			}
			return result(map[string]any{"state": state, "results": results}, failed)
		}
		response, err := w.ports.Mutate(ctx, bearer, request)
		if err != nil {
			return failureResult(err, true)
		}
		results, failed, err := projectMutationResults(response.Results)
		if err != nil {
			return nil, err
		}
		return result(map[string]any{"results": results}, failed)
	default:
		return result(map[string]string{"error": "UNKNOWN_TOOL"}, true)
	}
}

func lowerQuery(ctx context.Context, raw json.RawMessage, mine bool) (query.Request, error) {
	var input queryInput
	if err := decode(raw, &input); err != nil {
		return query.Request{}, err
	}
	if input.Limit == 0 {
		input.Limit = 50
	}
	if input.Limit > 200 {
		return query.Request{}, errors.New("page too large")
	}
	request := query.Request{ObjectID: input.Object, Limit: input.Limit, Offset: input.Offset, OrderBy: input.OrderBy}
	for _, field := range input.Fields {
		request.Selections = append(request.Selections, query.Selection{Kind: query.SelectionField, Field: field})
	}
	var predicates []query.Predicate
	for _, filter := range input.Filters {
		if filter.Operator == query.ComparisonIn || filter.Operator == query.ComparisonNotIn {
			return query.Request{}, errors.New("unsupported scalar operator")
		}
		value, err := queryScalar(filter.Value)
		if err != nil {
			return query.Request{}, err
		}
		predicates = append(predicates, query.Predicate{Kind: query.PredicateFieldComparison, Field: filter.Field, Operator: filter.Operator, Value: &value})
	}
	if mine {
		predicates = append(predicates, query.Predicate{Kind: query.PredicateComparison, Operator: query.ComparisonEqual,
			Left: query.Operand{Kind: query.OperandField, Field: "OwnerId"}, Right: query.Operand{Kind: query.OperandTrustedContext, TrustedContext: query.TrustedContextPrincipalID}})
	}
	if len(predicates) == 1 {
		request.Filter = predicates[0]
	} else if len(predicates) > 1 {
		request.Filter = query.Predicate{Kind: query.PredicateAnd, Children: predicates}
	}
	return request, query.ValidateRequest(ctx, request)
}

func queryScalar(raw json.RawMessage) (query.Scalar, error) {
	var v any
	d := json.NewDecoder(bytes.NewReader(raw))
	d.UseNumber()
	if err := d.Decode(&v); err != nil {
		return query.Scalar{}, err
	}
	switch v := v.(type) {
	case nil:
		return query.Scalar{Null: true}, nil
	case string:
		return query.Scalar{String: &v}, nil
	case bool:
		return query.Scalar{Boolean: &v}, nil
	case json.Number:
		// Decimal arithmetic is exact, including integers above 2^53. Runtime
		// validates its own supported precision and scale after lowering.
		text := strings.ToLower(string(v))
		exponent := 0
		if at := strings.IndexByte(text, 'e'); at >= 0 {
			var err error
			exponent, err = strconv.Atoi(text[at+1:])
			if err != nil || exponent < -128 || exponent > 128 {
				return query.Scalar{}, errors.New("exponent out of range")
			}
			text = text[:at]
		}
		scale := 0
		if at := strings.IndexByte(text, '.'); at >= 0 {
			scale = len(text) - at - 1
			text = text[:at] + text[at+1:]
		}
		scale -= exponent
		if scale < 0 {
			text += strings.Repeat("0", -scale)
			scale = 0
		}
		negative := strings.HasPrefix(text, "-")
		text = strings.TrimLeft(strings.TrimPrefix(text, "-"), "0")
		if text == "" {
			text = "0"
			negative = false
		}
		for scale > 0 && strings.HasSuffix(text, "0") && text != "0" {
			text = text[:len(text)-1]
			scale--
		}
		if text == "0" {
			scale = 0
		}
		if negative {
			text = "-" + text
		}
		return query.Scalar{Decimal: &query.Decimal{Coefficient: text, Scale: uint32(scale)}}, nil
	}
	return query.Scalar{}, errors.New("scalar required")
}

func lowerMutation(ctx context.Context, raw json.RawMessage) (mutation.Request, error) {
	var input mutationInput
	if err := decode(raw, &input); err != nil {
		return mutation.Request{}, err
	}
	if input.IdempotencyKey == "" || len(input.IdempotencyKey) > 128 || strings.TrimSpace(input.IdempotencyKey) != input.IdempotencyKey || input.Object == "" {
		return mutation.Request{}, errors.New("key and object required")
	}
	// The current single-record Runtime executor implements CRUD only. The
	// semantic contract's UPSERT arm is not proof of an executable capability.
	if input.Operation != mutation.OperationCreate && input.Operation != mutation.OperationUpdate && input.Operation != mutation.OperationDelete {
		return mutation.Request{}, errors.New("unsupported single-record operation")
	}
	key := sha256.Sum256([]byte("nexuscrm-mcp-v2\x00" + input.IdempotencyKey))
	operation := mutation.Operation{Kind: input.Operation, ObjectID: input.Object, RecordID: input.RecordID}
	names := make([]string, 0, len(input.Fields))
	for name := range input.Fields {
		names = append(names, name)
	}
	sort.Strings(names)
	for _, name := range names {
		raw := input.Fields[name]
		var value any
		d := json.NewDecoder(bytes.NewReader(raw))
		d.UseNumber()
		if err := d.Decode(&value); err != nil {
			return mutation.Request{}, err
		}
		field := mutation.FieldValue{Name: name}
		switch value := value.(type) {
		case nil:
			field.Kind, field.Null = mutation.ValueKindNull, true
		case string:
			field.Kind, field.String = mutation.ValueKindString, &value
		case bool:
			field.Kind, field.Boolean = mutation.ValueKindBoolean, &value
		case json.Number:
			text := string(value)
			field.Kind, field.Number = mutation.ValueKindNumber, &text
		default:
			text, err := mutation.CanonicalSourceJSON(string(raw))
			if err != nil {
				return mutation.Request{}, err
			}
			field.Kind, field.JSON = mutation.ValueKindJSON, &text
		}
		operation.Fields = append(operation.Fields, field)
	}
	request := mutation.Request{CommandID: "mcp-" + hex.EncodeToString(key[:]), BatchMode: mutation.BatchIndependent, UseCase: mutation.MutationUseCaseSingle, Operations: []mutation.Operation{operation}}
	return request, mutation.ValidateRequest(ctx, request)
}

func projectRecord(record query.Record) (map[string]any, error) {
	output := make(map[string]any, len(record.Fields))
	for _, field := range record.Fields {
		if _, found := output[field.Name]; found {
			return nil, errors.New("duplicate Runtime field")
		}
		value, err := projectValue(field.Value)
		if err != nil {
			return nil, err
		}
		output[field.Name] = value
	}
	return output, nil
}

func projectValue(value query.Value) (any, error) {
	if value.Null {
		return nil, nil
	}
	switch value.Kind {
	case query.ValueString:
		if value.String != nil {
			return *value.String, nil
		}
	case query.ValueBoolean:
		if value.Boolean != nil {
			return *value.Boolean, nil
		}
	case query.ValueInteger:
		if value.Integer != nil {
			return *value.Integer, nil
		}
	case query.ValueUnsigned:
		if value.Unsigned != nil {
			return *value.Unsigned, nil
		}
	case query.ValueDouble:
		if value.Double != nil {
			return *value.Double, nil
		}
	case query.ValueDate:
		if value.Date != nil {
			return *value.Date, nil
		}
	case query.ValueDateTime:
		if value.DateTime != nil {
			return *value.DateTime, nil
		}
	case query.ValueReference:
		if value.Reference != nil {
			return *value.Reference, nil
		}
	case query.ValueDecimal:
		if value.Decimal != nil {
			// Preserve exact decimals as JSON number tokens, not float64.
			return json.Number(value.Decimal.Coefficient + "e-" + strconv.FormatUint(uint64(value.Decimal.Scale), 10)), nil
		}
	case query.ValueObject:
		if value.Object != nil {
			return projectRecord(*value.Object)
		}
	case query.ValueList:
		if value.List != nil {
			items := make([]any, 0, len(*value.List))
			for _, item := range *value.List {
				projected, err := projectValue(item)
				if err != nil {
					return nil, err
				}
				items = append(items, projected)
			}
			return items, nil
		}
	}
	return nil, errors.New("invalid Runtime value")
}
