package repository

import (
	"context"
	"database/sql"
	"errors"
	"fmt"

	"github.com/charmingruby/homunc/apps/fraud/internal/fraud/model"
	"github.com/charmingruby/homunc/apps/fraud/pkg/database/postgres"
	"github.com/charmingruby/homunc/apps/fraud/pkg/telemetry"
	"github.com/jmoiron/sqlx"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
)

const (
	findBlockedSourceByCheckoutDetails = "find blocked source by checkout details"
	createBlockedSource                = "create blocked source"
	updateBlockedSource                = "update blocked source"
)

type BlockedSourcePostgresRepo struct {
	db    *sqlx.DB
	stmts map[string]*sqlx.Stmt
}

func NewBlockedSourcePostgresRepo(db *sqlx.DB) (*BlockedSourcePostgresRepo, error) {
	stmts := make(map[string]*sqlx.Stmt)

	for queryName, statement := range blockedSourceQueries() {
		stmt, err := db.Preparex(statement)
		if err != nil {
			return nil, postgres.NewPreparationErr(queryName, "blocked source", err)
		}

		stmts[queryName] = stmt
	}

	return &BlockedSourcePostgresRepo{
		db:    db,
		stmts: stmts,
	}, nil
}

func blockedSourceQueries() map[string]string {
	return map[string]string{
		findBlockedSourceByCheckoutDetails: `
		SELECT * FROM blocked_sources
		WHERE user_id = $1 AND session_id = $2 AND country = $3`,
		createBlockedSource: `
		INSERT INTO blocked_sources(
			id, session_id, user_id, country, attempts, created_at, updated_at, warned_at, blocked_until
		) VALUES(
			$1, $2, $3, $4, $5, $6, $7, $8, $9
		)`,
		updateBlockedSource: `
		UPDATE blocked_sources
		SET
			attempts = $1,
			blocked_until = $2,
			warned_at = $3,
			updated_at = $4
		WHERE id = $5`,
	}
}

func (r *BlockedSourcePostgresRepo) statement(queryName string) (*sqlx.Stmt, error) {
	stmt, ok := r.stmts[queryName]

	if !ok {
		return nil,
			postgres.NewStatementNotPreparedErr(queryName, "blocked source")
	}

	return stmt, nil
}

func (r *BlockedSourcePostgresRepo) FindByCheckoutDetails(ctx context.Context, checkoutDetails CheckoutDetails) (model.BlockedSource, error) {
	_, span := telemetry.Tracer.Start(ctx, "BlockedSourcePostgresRepo.FindByCheckoutDetails")
	defer span.End()

	span.SetAttributes(
		attribute.String("checkout.user_id", checkoutDetails.UserID),
		attribute.String("checkout.session_id", checkoutDetails.SessionID),
		attribute.String("checkout.country", checkoutDetails.Country),
	)

	stmt, err := r.statement(findBlockedSourceByCheckoutDetails)
	if err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, "Statement not prepared for find operation")

		return model.BlockedSource{}, postgres.NewStatementNotPreparedErr(findBlockedSourceByCheckoutDetails, "blocked source")
	}

	var result model.BlockedSource
	if err := stmt.GetContext(
		ctx,
		&result,
		checkoutDetails.UserID,
		checkoutDetails.SessionID,
		checkoutDetails.Country,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			span.SetStatus(codes.Ok, "No blocked source found for given details")
			span.SetAttributes(attribute.Bool("db.found_rows", false))

			return model.BlockedSource{}, nil
		}

		span.RecordError(err)
		span.SetStatus(codes.Error, fmt.Sprintf("Failed to get blocked source: %s", err.Error()))

		return model.BlockedSource{}, err
	}

	span.SetStatus(codes.Ok, "Blocked source found successfully")
	span.SetAttributes(attribute.Bool("db.found_rows", true))

	return result, nil
}

func (r *BlockedSourcePostgresRepo) Create(ctx context.Context, blockedSource model.BlockedSource) error {
	_, span := telemetry.Tracer.Start(ctx, "BlockedSourcePostgresRepo.Create")
	defer span.End()

	span.SetAttributes(
		attribute.String("blocked_source.id", blockedSource.ID),
		attribute.String("blocked_source.user_id", blockedSource.UserID),
		attribute.String("blocked_source.session_id", blockedSource.SessionID),
		attribute.String("blocked_source.country", blockedSource.Country),
	)

	stmt, err := r.statement(createBlockedSource)
	if err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, "Statement not prepared for create operation")

		return postgres.NewStatementNotPreparedErr(createBlockedSource, "blocked source")
	}

	if _, err := stmt.ExecContext(
		ctx,
		blockedSource.ID,
		blockedSource.SessionID,
		blockedSource.UserID,
		blockedSource.Country,
		blockedSource.Attempts,
		blockedSource.CreatedAt,
		blockedSource.UpdatedAt,
		blockedSource.WarnedAt,
		blockedSource.BlockedUntil,
	); err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, fmt.Sprintf("Failed to create blocked source: %s", err.Error()))

		return err
	}

	span.SetStatus(codes.Ok, "Blocked source created successfully")

	return nil
}

func (r *BlockedSourcePostgresRepo) Update(ctx context.Context, blockedSource model.BlockedSource) error {
	_, span := telemetry.Tracer.Start(ctx, "BlockedSourcePostgresRepo.Update")
	defer span.End()

	span.SetAttributes(
		attribute.String("blocked_source.id", blockedSource.ID),
		attribute.Int("blocked_source.attempts", blockedSource.Attempts),
		attribute.Bool("blocked_source.is_blocked", blockedSource.BlockedUntil != nil),
	)

	stmt, err := r.statement(updateBlockedSource)
	if err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, "Statement not prepared for update operation")

		return postgres.NewStatementNotPreparedErr(updateBlockedSource, "blocked source")
	}

	if _, err := stmt.ExecContext(
		ctx,
		blockedSource.Attempts,
		blockedSource.BlockedUntil,
		blockedSource.WarnedAt,
		blockedSource.UpdatedAt,
		blockedSource.ID,
	); err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, fmt.Sprintf("Failed to update blocked source: %s", err.Error()))

		return err
	}

	span.SetStatus(codes.Ok, "Blocked source updated successfully")

	return nil
}
