package issuetrackerconformance

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"net/http"
	"sort"
	"strconv"
	"strings"

	"github.com/nexuscrm/shared/pkg/contract"
	sharederrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
)

const IssueObject = "Issue"

type Outcome string

const (
	OutcomeOK        Outcome = "ok"
	OutcomeMissing   Outcome = "missing"
	OutcomeForbidden Outcome = "forbidden"
	OutcomeCanceled  Outcome = "canceled"
)

type FieldKind string

const (
	FieldString    FieldKind = "string"
	FieldBoolean   FieldKind = "boolean"
	FieldInteger   FieldKind = "integer"
	FieldDecimal   FieldKind = "decimal"
	FieldDate      FieldKind = "date"
	FieldDateTime  FieldKind = "datetime"
	FieldReference FieldKind = "reference"
)

type FieldSelection struct {
	Name string
	Kind FieldKind
}

type Request struct {
	RecordID string
	Fields   []FieldSelection
}

type RootField struct {
	Name  string
	Kind  FieldKind
	Null  bool
	Value any
}

type Result struct {
	Outcome Outcome
	Fields  []RootField
}

// PointReader is the single public neutral operation consumed by this fixture.
// contract.Records satisfies it without adding a capability plane.
type PointReader interface {
	Retrieve(context.Context, string, string, *models.UserSession) (contract.Record, error)
}

type ErrorClassifier func(error) (Outcome, bool)

// CurrentInProcessErrorClassifier adapts only the condition signals exposed by
// the current public in-process seam. The semantic outcomes are fixture-local;
// no status, error message, or product wire vocabulary enters Result.
func CurrentInProcessErrorClassifier(err error) (Outcome, bool) {
	if err == nil {
		return "", false
	}
	var wireErr *sharederrors.DomainWireError
	if errors.As(err, &wireErr) && wireErr.Kind == sharederrors.WireNotFound {
		return OutcomeMissing, true
	}
	if sharederrors.IsNotFound(err) {
		return OutcomeMissing, true
	}
	var statusErr interface{ HTTPStatus() int }
	if errors.As(err, &statusErr) {
		switch statusErr.HTTPStatus() {
		case http.StatusNotFound:
			return OutcomeMissing, true
		case http.StatusForbidden:
			return OutcomeForbidden, true
		}
	}
	return "", false
}

type Consumer struct {
	reader   PointReader
	classify ErrorClassifier
}

func NewConsumer(reader PointReader, classify ErrorClassifier) (*Consumer, error) {
	if reader == nil {
		return nil, fmt.Errorf("point reader is required")
	}
	if classify == nil {
		return nil, fmt.Errorf("error classifier is required")
	}
	return &Consumer{reader: reader, classify: classify}, nil
}

func (c *Consumer) Read(ctx context.Context, req Request, user *models.UserSession) (Result, error) {
	result := Result{Fields: []RootField{}}
	if c == nil || c.reader == nil || c.classify == nil {
		return result, fmt.Errorf("consumer is not configured")
	}
	recordID := strings.TrimSpace(req.RecordID)
	if recordID == "" {
		return result, fmt.Errorf("record identifier is required")
	}
	selection, err := normalizeSelection(req.Fields)
	if err != nil {
		return result, err
	}
	if errors.Is(ctx.Err(), context.Canceled) {
		result.Outcome = OutcomeCanceled
		return result, nil
	}

	record, err := c.reader.Retrieve(ctx, IssueObject, recordID, user)
	if err != nil {
		if errors.Is(err, context.Canceled) || errors.Is(ctx.Err(), context.Canceled) {
			result.Outcome = OutcomeCanceled
			return result, nil
		}
		if outcome, ok := c.classify(err); ok {
			result.Outcome = outcome
			return result, nil
		}
		return result, fmt.Errorf("point read failed: %w", err)
	}
	if record == nil {
		result.Outcome = OutcomeMissing
		return result, nil
	}

	result.Outcome = OutcomeOK
	for _, selected := range selection {
		raw, present := record[selected.Name]
		if !present {
			// The neutral record is already CRUD/RLS/FLS filtered. An absent
			// selected field remains omitted; the fixture does not guess why.
			continue
		}
		field := RootField{Name: selected.Name, Kind: selected.Kind}
		if raw == nil {
			field.Null = true
			result.Fields = append(result.Fields, field)
			continue
		}
		field.Value, err = normalizeValue(selected, raw)
		if err != nil {
			return Result{Fields: []RootField{}}, err
		}
		result.Fields = append(result.Fields, field)
	}
	return result, nil
}

func normalizeSelection(fields []FieldSelection) ([]FieldSelection, error) {
	if len(fields) == 0 {
		return nil, fmt.Errorf("at least one root field must be selected")
	}
	byName := make(map[string]FieldSelection, len(fields))
	for _, field := range fields {
		field.Name = strings.TrimSpace(field.Name)
		if field.Name == "" {
			return nil, fmt.Errorf("selected field name is required")
		}
		if !supportedKind(field.Kind) {
			return nil, fmt.Errorf("selected field %q has unsupported kind %q", field.Name, field.Kind)
		}
		if prior, exists := byName[field.Name]; exists && prior.Kind != field.Kind {
			return nil, fmt.Errorf("selected field %q has conflicting kinds %q and %q", field.Name, prior.Kind, field.Kind)
		}
		byName[field.Name] = field
	}
	out := make([]FieldSelection, 0, len(byName))
	for _, field := range byName {
		out = append(out, field)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
	return out, nil
}

func supportedKind(kind FieldKind) bool {
	switch kind {
	case FieldString, FieldBoolean, FieldInteger, FieldDecimal, FieldDate, FieldDateTime, FieldReference:
		return true
	default:
		return false
	}
}

func normalizeValue(field FieldSelection, raw any) (any, error) {
	switch field.Kind {
	case FieldString, FieldDate, FieldDateTime, FieldReference:
		value, ok := raw.(string)
		if !ok {
			return nil, fieldTypeError(field, raw)
		}
		return value, nil
	case FieldBoolean:
		value, ok := raw.(bool)
		if !ok {
			return nil, fieldTypeError(field, raw)
		}
		return value, nil
	case FieldInteger:
		value, ok := normalizeInteger(raw)
		if !ok {
			return nil, fieldTypeError(field, raw)
		}
		return value, nil
	case FieldDecimal:
		value, ok := normalizeDecimal(raw)
		if !ok {
			return nil, fieldTypeError(field, raw)
		}
		return value, nil
	default:
		return nil, fmt.Errorf("selected field %q has unsupported kind %q", field.Name, field.Kind)
	}
}

func normalizeInteger(raw any) (int64, bool) {
	switch value := raw.(type) {
	case int:
		return int64(value), true
	case int8:
		return int64(value), true
	case int16:
		return int64(value), true
	case int32:
		return int64(value), true
	case int64:
		return value, true
	case uint:
		if uint64(value) > math.MaxInt64 {
			return 0, false
		}
		return int64(value), true
	case uint8:
		return int64(value), true
	case uint16:
		return int64(value), true
	case uint32:
		return int64(value), true
	case uint64:
		if value > math.MaxInt64 {
			return 0, false
		}
		return int64(value), true
	case json.Number:
		parsed, err := strconv.ParseInt(value.String(), 10, 64)
		return parsed, err == nil
	default:
		return 0, false
	}
}

// Decimal values are kept as canonical text plus FieldDecimal. This avoids a
// float round-trip in the fixture while remaining a semantic test value rather
// than a wire representation.
func normalizeDecimal(raw any) (string, bool) {
	var text string
	switch value := raw.(type) {
	case json.Number:
		text = value.String()
	case float32:
		text = strconv.FormatFloat(float64(value), 'g', -1, 32)
	case float64:
		text = strconv.FormatFloat(value, 'g', -1, 64)
	case int:
		text = strconv.FormatInt(int64(value), 10)
	case int64:
		text = strconv.FormatInt(value, 10)
	default:
		return "", false
	}
	if _, err := strconv.ParseFloat(text, 64); err != nil {
		return "", false
	}
	return text, true
}

func fieldTypeError(field FieldSelection, raw any) error {
	return fmt.Errorf("selected field %q expected %s, got %T", field.Name, field.Kind, raw)
}
