package storage

import (
	"context"
	"errors"
	"strings"
	"time"

	"github.com/tech/sendico/edge/callbacks/internal/model"
	"github.com/tech/sendico/pkg/db"
	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/repository/builder"
	ri "github.com/tech/sendico/pkg/db/repository/index"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	pmodel "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mservice"
	mutil "github.com/tech/sendico/pkg/mutil/db"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

const (
	inboxCollection     string = "inbox"
	tasksCollection     string = "tasks"
	endpointsCollection string = mservice.Callbacks
)

type mongoRepository struct {
	logger mlogger.Logger

	inboxRepo     repository.Repository
	tasksRepo     repository.Repository
	endpointsRepo repository.Repository

	inbox     InboxRepo
	endpoints EndpointRepo
	tasks     TaskRepo
}

type inboxDoc struct {
	storable.Base                `bson:",inline"`
	pmodel.OrganizationBoundBase `bson:",inline"`
	EventID                      string `bson:"event_id"`
	EventType                    string `bson:"event_type"`
}

func (d *inboxDoc) Collection() string {
	return inboxCollection
}

type delayConfig struct {
	MinDelayMS int `bson:"min_ms"`
	MaxDelayMS int `bson:"max_ms"`
}

type deliveryPolicy struct {
	delayConfig      `bson:",inline"`
	SigningMode      string            `bson:"signing_mode"`
	SecretRef        string            `bson:"secret_ref"`
	Headers          map[string]string `bson:"headers"`
	MaxAttempts      int               `bson:"max_attempts"`
	RequestTimeoutMS int               `bson:"request_timeout_ms"`
}

type endpointDoc struct {
	storable.Base                `bson:",inline"`
	pmodel.OrganizationBoundBase `bson:",inline"`
	deliveryPolicy               `bson:"retry_policy"`
	Status                       string   `bson:"status"`
	URL                          string   `bson:"url"`
	EventTypes                   []string `bson:"event_types"`
}

func (d *endpointDoc) Collection() string {
	return endpointsCollection
}

type taskDoc struct {
	storable.Base  `bson:",inline"`
	deliveryPolicy `bson:"retry_policy"`
	EventID        string           `bson:"event_id"`
	EndpointRef    bson.ObjectID    `bson:"endpoint_ref"`
	EndpointURL    string           `bson:"endpoint_url"`
	Payload        []byte           `bson:"payload"`
	Status         model.TaskStatus `bson:"status"`
	Attempt        int              `bson:"attempt"`
	LastError      string           `bson:"last_error,omitempty"`
	LastHTTPCode   int              `bson:"last_http_code,omitempty"`
	NextAttemptAt  time.Time        `bson:"next_attempt_at"`
	LockedUntil    *time.Time       `bson:"locked_until,omitempty"`
	WorkerID       string           `bson:"worker_id,omitempty"`
	DeliveredAt    *time.Time       `bson:"delivered_at,omitempty"`
}

func (d *taskDoc) Collection() string {
	return tasksCollection
}

func newMongoRepository(logger mlogger.Logger, conn *db.MongoConnection) (Repository, error) {
	if logger == nil {
		logger = zap.NewNop()
	}
	if conn == nil {
		return nil, merrors.InvalidArgument("callbacks storage: mongo connection is required", "conn")
	}

	repo := &mongoRepository{
		logger:        logger.Named("storage"),
		inboxRepo:     repository.CreateMongoRepository(conn.Database(), inboxCollection),
		tasksRepo:     repository.CreateMongoRepository(conn.Database(), tasksCollection),
		endpointsRepo: repository.CreateMongoRepository(conn.Database(), endpointsCollection),
	}

	if err := repo.ensureIndexes(); err != nil {
		return nil, err
	}

	repo.inbox = &inboxStore{logger: repo.logger.Named(repo.inboxRepo.Collection()), repo: repo.inboxRepo}
	repo.endpoints = &endpointStore{logger: repo.logger.Named(repo.endpointsRepo.Collection()), repo: repo.endpointsRepo}
	repo.tasks = &taskStore{logger: repo.logger.Named(repo.tasksRepo.Collection()), repo: repo.tasksRepo}

	return repo, nil
}

func (m *mongoRepository) Inbox() InboxRepo {
	return m.inbox
}

func (m *mongoRepository) Endpoints() EndpointRepo {
	return m.endpoints
}

func (m *mongoRepository) Tasks() TaskRepo {
	return m.tasks
}

func (m *mongoRepository) ensureIndexes() error {
	if err := m.inboxRepo.CreateIndex(&ri.Definition{
		Name:   "uq_event_id",
		Unique: true,
		Keys: []ri.Key{
			{Field: "event_id", Sort: ri.Asc},
		},
	}); err != nil {
		return merrors.InternalWrap(err, "callbacks storage: failed to create inbox indexes")
	}

	for _, def := range []*ri.Definition{
		{
			Name:   "uq_event_endpoint",
			Unique: true,
			Keys: []ri.Key{
				{Field: "event_id", Sort: ri.Asc},
				{Field: "endpoint_ref", Sort: ri.Asc},
			},
		},
		{
			Name: "idx_dispatch_scan",
			Keys: []ri.Key{
				{Field: "status", Sort: ri.Asc},
				{Field: "next_attempt_at", Sort: ri.Asc},
				{Field: "locked_until", Sort: ri.Asc},
			},
		},
	} {
		if err := m.tasksRepo.CreateIndex(def); err != nil {
			return merrors.InternalWrap(err, "callbacks storage: failed to create tasks indexes")
		}
	}

	if err := m.endpointsRepo.CreateIndex(&ri.Definition{
		Name: "idx_client_event",
		Keys: []ri.Key{
			{Field: "organization_ref", Sort: ri.Asc},
			{Field: "status", Sort: ri.Asc},
			{Field: "event_types", Sort: ri.Asc},
		},
	}); err != nil {
		return merrors.InternalWrap(err, "callbacks storage: failed to create endpoint indexes")
	}

	return nil
}

type inboxStore struct {
	logger mlogger.Logger
	repo   repository.Repository
}

func (r *inboxStore) TryInsert(ctx context.Context, eventID, eventType string, organizationRef bson.ObjectID, at time.Time) (bool, error) {
	doc := &inboxDoc{
		OrganizationBoundBase: pmodel.OrganizationBoundBase{OrganizationRef: organizationRef},
		EventID:               strings.TrimSpace(eventID),
		EventType:             strings.TrimSpace(eventType),
	}

	filter := repository.Filter("event_id", doc.EventID)
	if err := r.repo.Insert(ctx, doc, filter); err != nil {
		if errors.Is(err, merrors.ErrDataConflict) {
			return false, nil
		}
		r.logger.Warn("Failed to insert inbox dedupe marker", zap.String("event_id", eventID), zap.Error(err))
		return false, merrors.InternalWrap(err, "callbacks inbox insert failed")
	}

	return true, nil
}

type endpointStore struct {
	logger mlogger.Logger
	repo   repository.Repository
}

func (r *endpointStore) FindActive(ctx context.Context, eventType string, organizationRef bson.ObjectID) ([]model.Endpoint, error) {
	eventType = strings.TrimSpace(eventType)
	if organizationRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("organization_ref is required", "organization_ref")
	}
	if eventType == "" {
		return nil, merrors.InvalidArgument("event type is required", "event_type")
	}

	query := repository.Query().
		Filter(repository.OrgField(), organizationRef).
		In(repository.Field("status"), "active", "enabled")

	out := make([]model.Endpoint, 0)
	err := r.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
		doc := &endpointDoc{}
		if err := cur.Decode(doc); err != nil {
			return err
		}
		if strings.TrimSpace(doc.URL) == "" {
			return nil
		}
		if !supportsEventType(doc.EventTypes, eventType) {
			return nil
		}
		out = append(out, model.Endpoint{
			Base:                  doc.Base,
			OrganizationBoundBase: doc.OrganizationBoundBase,
			URL:                   strings.TrimSpace(doc.URL),
			SigningMode:           strings.TrimSpace(doc.SigningMode),
			SecretRef:             strings.TrimSpace(doc.SecretRef),
			Headers:               cloneHeaders(doc.Headers),
			MaxAttempts:           doc.MaxAttempts,
			MinDelay:              time.Duration(doc.MinDelayMS) * time.Millisecond,
			MaxDelay:              time.Duration(doc.MaxDelayMS) * time.Millisecond,
			RequestTimeout:        time.Duration(doc.RequestTimeoutMS) * time.Millisecond,
		})
		return nil
	})
	if err != nil && !errors.Is(err, merrors.ErrNoData) {
		return nil, merrors.InternalWrap(err, "callbacks endpoint lookup failed")
	}

	return out, nil
}

func supportsEventType(eventTypes []string, eventType string) bool {
	if len(eventTypes) == 0 {
		return true
	}
	eventType = strings.TrimSpace(eventType)
	for _, t := range eventTypes {
		current := strings.TrimSpace(t)
		if current == "" {
			continue
		}
		if current == "*" || current == eventType {
			return true
		}
	}
	return false
}

type taskStore struct {
	logger mlogger.Logger
	repo   repository.Repository
}

func (r *taskStore) UpsertTasks(ctx context.Context, eventID string, endpoints []model.Endpoint, payload []byte, defaults TaskDefaults, at time.Time) error {
	eventID = strings.TrimSpace(eventID)
	if eventID == "" {
		return merrors.InvalidArgument("event id is required", "event_id")
	}
	if len(endpoints) == 0 {
		return nil
	}

	now := at.UTC()
	for _, endpoint := range endpoints {
		if endpoint.GetID() == nil || *endpoint.GetID() == bson.NilObjectID {
			continue
		}

		maxAttempts := endpoint.MaxAttempts
		if maxAttempts <= 0 {
			maxAttempts = defaults.MaxAttempts
		}
		if maxAttempts <= 0 {
			maxAttempts = 1
		}

		minDelay := endpoint.MinDelay
		if minDelay <= 0 {
			minDelay = defaults.MinDelay
		}
		if minDelay <= 0 {
			minDelay = time.Second
		}

		maxDelay := endpoint.MaxDelay
		if maxDelay <= 0 {
			maxDelay = defaults.MaxDelay
		}
		if maxDelay < minDelay {
			maxDelay = minDelay
		}

		requestTimeout := endpoint.RequestTimeout
		if requestTimeout <= 0 {
			requestTimeout = defaults.RequestTimeout
		}

		doc := &taskDoc{}
		doc.EventID = eventID
		doc.EndpointRef = *endpoint.GetID()
		doc.EndpointURL = strings.TrimSpace(endpoint.URL)
		doc.SigningMode = strings.TrimSpace(endpoint.SigningMode)
		doc.SecretRef = strings.TrimSpace(endpoint.SecretRef)
		doc.Headers = cloneHeaders(endpoint.Headers)
		doc.Payload = append([]byte(nil), payload...)
		doc.Status = model.TaskStatusPending
		doc.Attempt = 0
		doc.MaxAttempts = maxAttempts
		doc.MinDelayMS = int(minDelay / time.Millisecond)
		doc.MaxDelayMS = int(maxDelay / time.Millisecond)
		doc.RequestTimeoutMS = int(requestTimeout / time.Millisecond)
		doc.NextAttemptAt = now

		filter := repository.Filter("event_id", eventID).And(repository.Filter("endpoint_ref", endpoint.ID))
		if err := r.repo.Insert(ctx, doc, filter); err != nil {
			if errors.Is(err, merrors.ErrDataConflict) {
				continue
			}
			return merrors.InternalWrap(err, "callbacks task upsert failed")
		}
	}

	return nil
}

func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID string, lockTTL time.Duration) (*model.Task, error) {
	workerID = strings.TrimSpace(workerID)
	if workerID == "" {
		return nil, merrors.InvalidArgument("worker id is required", "worker_id")
	}

	now = now.UTC()
	limit := int64(32)
	lockFilter := repository.Query().Or(
		repository.Query().Comparison(repository.Field("locked_until"), builder.Exists, false),
		repository.Query().Filter(repository.Field("locked_until"), nil),
		repository.Query().Comparison(repository.Field("locked_until"), builder.Lte, now),
	)

	query := repository.Query().
		In(repository.Field("status"), string(model.TaskStatusPending), string(model.TaskStatusRetry)).
		Comparison(repository.Field("next_attempt_at"), builder.Lte, now).
		And(lockFilter).
		Sort(repository.Field("next_attempt_at"), true).
		Sort(repository.Field("created_at"), true).
		Limit(&limit)

	candidates, err := mutil.GetObjects[taskDoc](ctx, r.logger, query, nil, r.repo)
	if err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			return nil, merrors.ErrNoData
		}
		return nil, merrors.InternalWrap(err, "callbacks task query failed")
	}

	lockedUntil := now.Add(lockTTL)
	for _, candidate := range candidates {
		patch := repository.Patch().
			Set(repository.Field("locked_until"), lockedUntil).
			Set(repository.Field("worker_id"), workerID)

		conditional := repository.IDFilter(candidate.ID).And(
			repository.Query().In(repository.Field("status"), string(model.TaskStatusPending), string(model.TaskStatusRetry)),
			repository.Query().Comparison(repository.Field("next_attempt_at"), builder.Lte, now),
			lockFilter,
		)

		updated, err := r.repo.PatchMany(ctx, conditional, patch)
		if err != nil {
			return nil, merrors.InternalWrap(err, "callbacks task lock update failed")
		}
		if updated == 0 {
			continue
		}

		locked := &taskDoc{}
		if err := r.repo.Get(ctx, candidate.ID, locked); err != nil {
			if errors.Is(err, merrors.ErrNoData) {
				continue
			}
			return nil, merrors.InternalWrap(err, "callbacks task lock reload failed")
		}
		if strings.TrimSpace(locked.WorkerID) != workerID {
			continue
		}

		return mapTaskDoc(locked), nil
	}

	return nil, merrors.ErrNoData
}

func (r *taskStore) MarkDelivered(ctx context.Context, taskID bson.ObjectID, httpCode int, latency time.Duration, at time.Time) error {
	_ = latency
	if taskID == bson.NilObjectID {
		return merrors.InvalidArgument("task id is required", "task_id")
	}

	patch := repository.Patch().
		Set(repository.Field("status"), model.TaskStatusDelivered).
		Set(repository.Field("last_http_code"), httpCode).
		Set(repository.Field("delivered_at"), time.Now()).
		Set(repository.Field("locked_until"), nil).
		Set(repository.Field("worker_id"), "").
		Set(repository.Field("last_error"), "")

	if err := r.repo.Patch(ctx, taskID, patch); err != nil {
		return merrors.InternalWrap(err, "callbacks task mark delivered failed")
	}
	return nil
}

func (r *taskStore) MarkRetry(ctx context.Context, taskID bson.ObjectID, attempt int, nextAttemptAt time.Time, lastError string, httpCode int, at time.Time) error {
	if taskID == bson.NilObjectID {
		return merrors.InvalidArgument("task id is required", "task_id")
	}

	patch := repository.Patch().
		Set(repository.Field("status"), model.TaskStatusRetry).
		Set(repository.Field("attempt"), attempt).
		Set(repository.Field("next_attempt_at"), nextAttemptAt.UTC()).
		Set(repository.Field("last_error"), strings.TrimSpace(lastError)).
		Set(repository.Field("last_http_code"), httpCode).
		Set(repository.Field("locked_until"), nil).
		Set(repository.Field("worker_id"), "")

	if err := r.repo.Patch(ctx, taskID, patch); err != nil {
		return merrors.InternalWrap(err, "callbacks task mark retry failed")
	}
	return nil
}

func (r *taskStore) MarkFailed(ctx context.Context, taskID bson.ObjectID, attempt int, lastError string, httpCode int, at time.Time) error {
	if taskID == bson.NilObjectID {
		return merrors.InvalidArgument("task id is required", "task_id")
	}

	patch := repository.Patch().
		Set(repository.Field("status"), model.TaskStatusFailed).
		Set(repository.Field("attempt"), attempt).
		Set(repository.Field("last_error"), strings.TrimSpace(lastError)).
		Set(repository.Field("last_http_code"), httpCode).
		Set(repository.Field("locked_until"), nil).
		Set(repository.Field("worker_id"), "")

	if err := r.repo.Patch(ctx, taskID, patch); err != nil {
		return merrors.InternalWrap(err, "callbacks task mark failed failed")
	}
	return nil
}

func mapTaskDoc(doc *taskDoc) *model.Task {
	if doc == nil {
		return nil
	}
	return &model.Task{
		Base:           doc.Base,
		EventID:        doc.EventID,
		EndpointRef:    doc.EndpointRef,
		EndpointURL:    doc.EndpointURL,
		SigningMode:    doc.SigningMode,
		SecretRef:      doc.SecretRef,
		Headers:        cloneHeaders(doc.Headers),
		Payload:        append([]byte(nil), doc.Payload...),
		Attempt:        doc.Attempt,
		MaxAttempts:    doc.MaxAttempts,
		MinDelay:       time.Duration(doc.MinDelayMS) * time.Millisecond,
		MaxDelay:       time.Duration(doc.MaxDelayMS) * time.Millisecond,
		RequestTimeout: time.Duration(doc.RequestTimeoutMS) * time.Millisecond,
		Status:         doc.Status,
		NextAttemptAt:  doc.NextAttemptAt,
	}
}

func cloneHeaders(in map[string]string) map[string]string {
	if len(in) == 0 {
		return map[string]string{}
	}
	out := make(map[string]string, len(in))
	for key, val := range in {
		out[key] = val
	}
	return out
}
