package repository

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

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/warmbly/warmbly/internal/config"
	"github.com/warmbly/warmbly/internal/errx"
	"github.com/warmbly/warmbly/internal/infrastructure/db"
	"github.com/warmbly/warmbly/internal/models"
	"github.com/warmbly/warmbly/internal/pkg/encrypt"
)

type SequenceRepository interface {
	Create(ctx context.Context, userID, campaignID string) (*models.Sequence, *errx.Error)
	Get(ctx context.Context, userID, campaignID string) ([]models.Sequence, *errx.Error)
	Update(ctx context.Context, userID, campaignID, sequenceID string, data *models.UpdateSequence) (*models.Sequence, *errx.Error)
	// UpdateLayout merges only x/y for the given steps under a campaign, in one
	// statement, without bumping updated_at — so a drag never reads as a content
	// change. Cosmetic and unaudited.
	UpdateLayout(ctx context.Context, userID, campaignID string, positions []models.SequencePosition) *errx.Error
	Delete(ctx context.Context, userID, campaignID, sequenceID string) *errx.Error
}

type sequenceRepository struct {
	DB      *db.DB
	Encrypt *encrypt.Encrypter
}

func NewSequenceRepostory(db *db.DB) SequenceRepository {
	return &sequenceRepository{
		DB: db,
	}
}

var SequenceSelections []string = []string{
	"id",
	"name",
	"subject",
	"body_plain",
	"body_html",
	"body_sync",
	"body_code",
	"wait_after",
	"position",
	"x",
	"y",
	"conditions",
	"kind",
	"action",
	"updated_at",
	"created_at",
}

func getSequenceSelect(join bool) string {
	sel := SequenceSelections
	if join {
		for i := range sel {
			sel[i] = "s." + sel[i]
		}
	}
	return strings.Join(sel, ", ")
}

var (
	SequenceSelect     = getSequenceSelect(false)
	SequenceSelectJoin = getSequenceSelect(true)
)

func GetSequence(row db.Scannable, seq *models.Sequence) error {
	return row.Scan(
		&seq.ID, &seq.Name, &seq.Subject, &seq.BodyPlain, &seq.BodyHTML, &seq.BodySync,
		&seq.BodyCode, &seq.WaitAfter, &seq.Position, &seq.X, &seq.Y, &seq.Conditions, &seq.Kind, &seq.Action,
		&seq.UpdatedAt, &seq.CreatedAt,
	)
}

func (r *sequenceRepository) Get(ctx context.Context, userID string, campaignID string) ([]models.Sequence, *errx.Error) {
	query := fmt.Sprintf(
		`SELECT %s
		 FROM sequences s
		 JOIN campaigns c ON s.campaign_id = c.id
		 WHERE s.campaign_id = $1
		  AND c.user_id = $2
		 ORDER BY s.position ASC, s.created_at ASC`,
		SequenceSelectJoin,
	)

	params := []any{
		campaignID,
		userID,
	}

	rows, err := r.DB.Query(
		ctx,
		query,
		params...,
	)
	if err != nil {
		db.CaptureError(err, query, params, "query")
		return nil, errx.InternalError()
	}

	var sequences []models.Sequence = make([]models.Sequence, 0)

	for rows.Next() {
		var seq models.Sequence
		err = GetSequence(rows, &seq)
		if err != nil {
			db.CaptureError(err, "", nil, "scan")
			return nil, errx.InternalError()
		}
		sequences = append(sequences, seq)
	}

	return sequences, nil
}

func (r *sequenceRepository) Create(ctx context.Context, userID string, campaignID string) (*models.Sequence, *errx.Error) {
	tx, err := r.DB.Begin(ctx)
	if err != nil {
		db.CaptureError(err, "", nil, "begin")
		return nil, errx.InternalError()
	}
	defer tx.Rollback(ctx)

	// FOR UPDATE serialises step creation per campaign, so two concurrent
	// inserts on a one-time campaign cannot both see zero email steps.
	query := `
		SELECT user_id, organization_id, kind
		FROM campaigns WHERE id = $1
		FOR UPDATE
	`

	params := []any{
		campaignID,
	}

	var ownerID string
	var orgID uuid.UUID
	var kind string
	err = tx.QueryRow(
		ctx,
		query,
		params...,
	).Scan(&ownerID, &orgID, &kind)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errx.ErrNotFound
		}
		db.CaptureError(err, query, params, "queryrow")
		return nil, errx.InternalError()
	}

	if ownerID != userID {
		return nil, errx.ErrForbidden
	}

	// A one-time email is a single message: a second email step would turn
	// it into a sequence without the list, status wording or docs saying so.
	if kind == models.CampaignKindOneTime {
		var emailSteps int
		if err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM sequences WHERE campaign_id = $1 AND kind = 'email'`, campaignID).Scan(&emailSteps); err != nil {
			db.CaptureError(err, "", params, "queryrow")
			return nil, errx.InternalError()
		}
		if emailSteps > 0 {
			return nil, errx.New(errx.BadRequest, "a one-time email has a single message; create a sequence campaign for follow-ups")
		}
	}

	// Get the next position for this campaign's sequences
	var nextPos int
	_ = tx.QueryRow(ctx, `SELECT COALESCE(MAX(position), 0) + 1 FROM sequences WHERE campaign_id = $1`, campaignID).Scan(&nextPos)

	// organization_id is inherited from the campaign: a step that lost it is
	// invisible to every org-scoped read and skips org-scoped send gates.
	query = fmt.Sprintf(
		`INSERT INTO sequences (
			campaign_id, organization_id, name, subject, body_plain, body_html, position
		 ) VALUES (
			$1, $2, $3, $4, $5, $6, $7
		 ) RETURNING %s`, SequenceSelect,
	)

	params = []any{
		campaignID,
		orgID,
		config.SequenceDefaultName,
		"",
		"",
		"<div></div>",
		nextPos,
	}

	row := tx.QueryRow(
		ctx,
		query,
		params...,
	)
	var seq models.Sequence
	err = GetSequence(row, &seq)
	if err != nil {
		db.CaptureError(err, query, params, "scan")
		return nil, errx.InternalError()
	}

	if err := tx.Commit(ctx); err != nil {
		db.CaptureError(err, "", nil, "commit")
		return nil, errx.InternalError()
	}

	return &seq, nil
}

func (r *sequenceRepository) Update(ctx context.Context, userID, campaignID, sequenceID string, data *models.UpdateSequence) (*models.Sequence, *errx.Error) {
	setClauses := []string{}
	args := []any{userID, campaignID, sequenceID}
	argPos := 4

	if data.Name != nil {
		if len(*data.Name) > 50 {
			return nil, errx.ErrSequenceName
		}
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "name", argPos))
		args = append(args, *data.Name)
		argPos++
	}
	if data.Subject != nil {
		if len(*data.Subject) > 100 {
			return nil, errx.ErrSequenceSubject
		}
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "subject", argPos))
		args = append(args, *data.Subject)
		argPos++
	}
	if data.BodyPlain != nil {
		if len(*data.BodyPlain) > config.SequenceBodyLimit {
			return nil, errx.ErrSequenceBody
		}
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "body_plain", argPos))
		args = append(args, *data.BodyPlain)
		argPos++
	}
	if data.BodyHTML != nil {
		if len(*data.BodyHTML) > config.SequenceBodyLimit {
			return nil, errx.ErrSequenceBody
		}
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "body_html", argPos))
		args = append(args, *data.BodyHTML)
		argPos++
	}
	if data.BodySync != nil {
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "body_sync", argPos))
		args = append(args, *data.BodySync)
		argPos++
	}
	if data.BodyCode != nil {
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "body_code", argPos))
		args = append(args, *data.BodyCode)
		argPos++
	}
	if data.WaitAfter != nil {
		if *data.WaitAfter < 0 || *data.WaitAfter > config.SequenceWaitAfterMax {
			return nil, errx.ErrSequenceWaitAfter
		}
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "wait_after", argPos))
		args = append(args, *data.WaitAfter)
		argPos++
	}
	if data.Conditions != nil {
		// Validate shape (operators/fields/values) before persisting. Cross-step
		// validation (targets-in-campaign + no cycles) happens in the service
		// layer where the full sequence set is available.
		if verr := validateBranchConditions(data.Conditions); verr != nil {
			return nil, verr
		}
		raw, merr := json.Marshal(data.Conditions)
		if merr != nil {
			db.CaptureError(merr, "", nil, "marshal_conditions")
			return nil, errx.InternalError()
		}
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "conditions", argPos))
		args = append(args, raw)
		argPos++
	}
	if data.Kind != nil {
		if *data.Kind != "email" && *data.Kind != "action" && *data.Kind != "wait" {
			return nil, errx.ErrSequenceKind
		}
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "kind", argPos))
		args = append(args, *data.Kind)
		argPos++
	}
	if data.Action != nil {
		if verr := validateActionConfig(data.Action); verr != nil {
			return nil, verr
		}
		raw, merr := json.Marshal(data.Action)
		if merr != nil {
			db.CaptureError(merr, "", nil, "marshal_action")
			return nil, errx.InternalError()
		}
		setClauses = append(setClauses, fmt.Sprintf("%s = $%d", "action", argPos))
		args = append(args, raw)
		argPos++
	}

	if argPos == 4 {
		return nil, errx.ErrNotEnough
	}

	var seq models.Sequence
	query := fmt.Sprintf(
		`UPDATE sequences s
		 SET %s
		 FROM campaigns c
		 WHERE c.user_id = $1
		  AND c.id = $2
		  AND s.id = $3
		 RETURNING %s`,
		strings.Join(setClauses, ", "),
		SequenceSelectJoin,
	)

	row := r.DB.QueryRow(
		ctx,
		query,
		args...,
	)
	err := GetSequence(row, &seq)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errx.ErrNotFound
		}
		db.CaptureError(err, query, args, "queryrow")
		return nil, errx.InternalError()
	}

	return &seq, nil
}

// UpdateLayout writes only canvas coordinates for a batch of steps under one
// campaign, in a single statement scoped to the campaign owner. It leaves every
// content column (and updated_at) untouched, so a position move never reads as a
// content change to teammates. Unknown ids are silently ignored.
func (r *sequenceRepository) UpdateLayout(ctx context.Context, userID, campaignID string, positions []models.SequencePosition) *errx.Error {
	ids := make([]uuid.UUID, 0, len(positions))
	xs := make([]float64, 0, len(positions))
	ys := make([]float64, 0, len(positions))
	for _, p := range positions {
		id, err := uuid.Parse(p.ID)
		if err != nil {
			continue
		}
		ids = append(ids, id)
		xs = append(xs, p.X)
		ys = append(ys, p.Y)
	}
	if len(ids) == 0 {
		return nil
	}

	query := `
		UPDATE sequences AS s
		SET x = v.x, y = v.y
		FROM unnest($3::uuid[], $4::float8[], $5::float8[]) AS v(id, x, y)
		WHERE s.id = v.id
		  AND s.campaign_id = $2
		  AND EXISTS (SELECT 1 FROM campaigns c WHERE c.id = $2 AND c.user_id = $1)`
	args := []any{userID, campaignID, ids, xs, ys}
	if _, err := r.DB.Exec(ctx, query, args...); err != nil {
		db.CaptureError(err, query, args, "exec")
		return errx.InternalError()
	}
	return nil
}

func (r *sequenceRepository) Delete(ctx context.Context, userID, campaignID, sequenceID string) *errx.Error {
	query := `
		DELETE FROM sequences s
		USING campaigns c
		WHERE c.user_id = $1
		 AND c.id = $2
		 AND s.id = $3
	`

	params := []any{
		userID,
		campaignID,
		sequenceID,
	}

	cmd, err := r.DB.Exec(
		ctx,
		query,
		params...,
	)
	if err != nil {
		db.CaptureError(err, query, params, "exec")
		return errx.InternalError()
	}
	if cmd.RowsAffected() == 0 {
		return errx.ErrNotFound
	}
	return nil
}
