package payments

import (
	"context"
	"crussell/db"
	"crussell/clock"
	"crussell/internal/square"
	"errors"
	"fmt"
	"log"
	"log/slog"
	"strconv"
	"strings"
	"time"

	"github.com/jackc/pgx/v5"
)

type SavedCard struct {
	ID           string `json:"id"`
	SquareCardID string `json:"square_card_id"`
	Brand        string `json:"brand"`
	Last4        string `json:"last_4"`
	ExpMonth     int    `json:"exp_month"`
	ExpYear      int    `json:"exp_year"`
	Fingerprint  string `json:"fingerprint"`
	IsDefault    bool   `json:"is_default"`
}

type PaymentService struct{}

func NewPaymentService() *PaymentService {
	return &PaymentService{}
}

type PaymentRecord struct {
	ID              string
	BookingID       string
	PaymentType     string
	PaymentMethod   string
	VendorCode      *string
	InvoiceNumber   *int
	Status          string
	Amount          float64
	CardLast4       string
	IsVATApplicable bool
	VATRate         *float64
	VATAmount       *float64
	NetAmount       *float64
	UserSavedCardID *string
	SquarePaymentID *string
	IdempotencyKey  *string
	Fees            float64
	CreatedAt       time.Time
	UpdatedAt       time.Time
	CreatedBy       *string
	GiftCardID      *string
}

type RefundRecord struct {
	ID             string
	PaymentID      string
	BookingID      string
	Amount         float64
	SquareRefundID *string
	Status         string
	Reason         string
	CreatedBy      *string
	CreatedAt      time.Time
}

type PaymentSummary struct {
	TotalAmount     float64
	PaidAmount      float64
	RefundedAmount  float64
	RemainingAmount float64
	TotalVATAmount  float64
	TotalNetAmount  float64
	Payments        []PaymentRecord
	Refunds         []RefundRecord
}

func (s *PaymentService) CalculateFees(amount int64, method string) float64 {
	if method == "online" {
		return float64((amount*14/1000)+25) / 100.0
	}
	return float64(amount*175/10000) / 100.0
}

func (s *PaymentService) CreatePaymentRecord(ctx context.Context, record PaymentRecord, giftCardID *string) (string, error) {
	return s.insertPaymentRecord(ctx, record, giftCardID, db.Conn)
}

// CreatePaymentRecordTx is identical to CreatePaymentRecord but accepts a
// pgx.Tx so the insert is part of an existing database transaction. This
// is used by CreateBookingPayment when inserting multiple split records
// from a single Square charge — wrapping both inserts in a transaction
// ensures atomicity (both succeed or both roll back).
func (s *PaymentService) CreatePaymentRecordTx(ctx context.Context, tx pgx.Tx, record PaymentRecord, giftCardID *string) (string, error) {
	return s.insertPaymentRecord(ctx, record, giftCardID, tx)
}

// insertPaymentRecord holds the common INSERT logic. The querier parameter
// accepts either *pgxpool.Pool or pgx.Tx so callers can choose transactional
// or non-transactional insertion.
type querier interface {
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

func (s *PaymentService) insertPaymentRecord(ctx context.Context, record PaymentRecord, giftCardID *string, q querier) (string, error) {
	var bookingID *string
	if record.BookingID != "" {
		bookingID = &record.BookingID
	}

	var id string
	err := q.QueryRow(ctx, `
		INSERT INTO payments (
			booking_id, payment_type, payment_method, vendor_code, invoice_number,
			status, amount, is_vat_applicable, vat_rate, vat_amount, net_amount,
			user_saved_card_id, square_payment_id, idempotency_key, fees, created_at, updated_at, created_by,
			gift_card_id
		) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
		RETURNING id
	`,
		bookingID,
		record.PaymentType,
		record.PaymentMethod,
		record.VendorCode,
		record.InvoiceNumber,
		record.Status,
		record.Amount,
		record.IsVATApplicable,
		record.VATRate,
		record.VATAmount,
		record.NetAmount,
		record.UserSavedCardID,
		record.SquarePaymentID,
		record.IdempotencyKey,
		record.Fees,
		record.CreatedAt,
		record.UpdatedAt,
		record.CreatedBy,
		giftCardID,
	).Scan(&id)

	if err != nil {
		return "", err
	}
	return id, nil
}

func (s *PaymentService) CreateRefundRecord(ctx context.Context, record RefundRecord) (string, error) {
	var id string
	err := db.Conn.QueryRow(ctx, `
		INSERT INTO refunds (
			payment_id, booking_id, amount, square_refund_id, status, reason, created_by, created_at
		) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
		RETURNING id
	`,
		record.PaymentID,
		record.BookingID,
		record.Amount,
		record.SquareRefundID,
		record.Status,
		record.Reason,
		record.CreatedBy,
		record.CreatedAt,
	).Scan(&id)

	if err != nil {
		return "", err
	}
	return id, nil
}

func (s *PaymentService) GetBookingPaymentSummary(ctx context.Context, bookingID string) (*PaymentSummary, error) {
	summary := &PaymentSummary{
		Payments: []PaymentRecord{},
		Refunds:  []RefundRecord{},
	}

	var totalAmount float64
	err := db.Conn.QueryRow(ctx, `
		SELECT total_amount FROM bookings WHERE id = $1
	`, bookingID).Scan(&totalAmount)

	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return nil, err
	}
	summary.TotalAmount = totalAmount

	rows, err := db.Conn.Query(ctx, `
		SELECT p.id, p.booking_id, p.payment_type, p.payment_method, p.vendor_code, p.invoice_number,
		       p.status, p.amount, COALESCE(usc.last_4, ''), p.is_vat_applicable, p.vat_rate, p.vat_amount, p.net_amount,
		       p.user_saved_card_id, p.square_payment_id, p.idempotency_key, p.fees, p.created_at, p.updated_at, p.created_by,
		       p.gift_card_id
		FROM payments p
		LEFT JOIN user_saved_cards usc ON p.user_saved_card_id = usc.id
		WHERE p.booking_id = $1
		ORDER BY p.created_at ASC
	`, bookingID)

	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var paidAmount, totalVATAmount, totalNetAmount float64
	for rows.Next() {
		var p PaymentRecord
		err := rows.Scan(
			&p.ID, &p.BookingID, &p.PaymentType, &p.PaymentMethod, &p.VendorCode, &p.InvoiceNumber,
			&p.Status, &p.Amount, &p.CardLast4, &p.IsVATApplicable, &p.VATRate, &p.VATAmount, &p.NetAmount,
			&p.UserSavedCardID, &p.SquarePaymentID, &p.IdempotencyKey, &p.Fees,
			&p.CreatedAt, &p.UpdatedAt, &p.CreatedBy, &p.GiftCardID,
		)
		if err != nil {
			return nil, err
		}
		summary.Payments = append(summary.Payments, p)
		if p.Status == "completed" {
			paidAmount += p.Amount
			if p.VATAmount != nil {
				totalVATAmount += *p.VATAmount
			}
			if p.NetAmount != nil {
				totalNetAmount += *p.NetAmount
			} else if p.VATAmount == nil {
				// Only fallback to gross amount if no VAT was applied at all.
				// When VAT is present, net_amount is always set by apply_vat_to_payment,
				// so this fallback only applies to non-VAT payments where net == gross.
				totalNetAmount += p.Amount
			}
		}
	}
	summary.PaidAmount = paidAmount
	summary.TotalVATAmount = totalVATAmount
	summary.TotalNetAmount = totalNetAmount

	refundRows, err := db.Conn.Query(ctx, `
		SELECT id, payment_id, booking_id, amount, square_refund_id, status, reason, created_by, created_at
		FROM refunds
		WHERE booking_id = $1 AND status = 'completed'
		ORDER BY created_at ASC
	`, bookingID)

	if err != nil {
		return nil, err
	}
	defer refundRows.Close()

	var refundedAmount float64
	for refundRows.Next() {
		var r RefundRecord
		err := refundRows.Scan(
			&r.ID, &r.PaymentID, &r.BookingID, &r.Amount, &r.SquareRefundID,
			&r.Status, &r.Reason, &r.CreatedBy, &r.CreatedAt,
		)
		if err != nil {
			return nil, err
		}
		summary.Refunds = append(summary.Refunds, r)
		refundedAmount += r.Amount
	}
	summary.RefundedAmount = refundedAmount
	summary.RemainingAmount = totalAmount - paidAmount + refundedAmount

	return summary, nil
}

func (s *PaymentService) CheckIdempotency(ctx context.Context, bookingID, idempotencyKey string) (*PaymentRecord, error) {
	var p PaymentRecord
	err := db.Conn.QueryRow(ctx, `
		SELECT id, booking_id, payment_type, payment_method, vendor_code, invoice_number,
		       status, amount, is_vat_applicable, vat_rate, vat_amount, net_amount,
		       user_saved_card_id, square_payment_id, idempotency_key, fees, created_at, updated_at, created_by,
		       gift_card_id
		FROM payments
		WHERE booking_id = $1 AND idempotency_key = $2
	`, bookingID, idempotencyKey).Scan(
		&p.ID, &p.BookingID, &p.PaymentType, &p.PaymentMethod, &p.VendorCode, &p.InvoiceNumber,
		&p.Status, &p.Amount, &p.IsVATApplicable, &p.VATRate, &p.VATAmount, &p.NetAmount,
		&p.UserSavedCardID, &p.SquarePaymentID, &p.IdempotencyKey, &p.Fees,
		&p.CreatedAt, &p.UpdatedAt, &p.CreatedBy, &p.GiftCardID,
	)

	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, nil
		}
		return nil, err
	}
	return &p, nil
}

func (s *PaymentService) CheckIdempotencyByKey(ctx context.Context, idempotencyKey string) (*PaymentRecord, error) {
	var p PaymentRecord
	err := db.Conn.QueryRow(ctx, `
		SELECT id, booking_id, payment_type, payment_method, vendor_code, invoice_number,
		       status, amount, is_vat_applicable, vat_rate, vat_amount, net_amount,
		       user_saved_card_id, square_payment_id, idempotency_key, fees, created_at, updated_at, created_by,
		       gift_card_id
		FROM payments
		WHERE idempotency_key = $1
	`, idempotencyKey).Scan(
		&p.ID, &p.BookingID, &p.PaymentType, &p.PaymentMethod, &p.VendorCode, &p.InvoiceNumber,
		&p.Status, &p.Amount, &p.IsVATApplicable, &p.VATRate, &p.VATAmount, &p.NetAmount,
		&p.UserSavedCardID, &p.SquarePaymentID, &p.IdempotencyKey, &p.Fees,
		&p.CreatedAt, &p.UpdatedAt, &p.CreatedBy, &p.GiftCardID,
	)

	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, nil
		}
		return nil, err
	}
	return &p, nil
}

func (s *PaymentService) GetPaymentByID(ctx context.Context, paymentID string) (*PaymentRecord, error) {
	var p PaymentRecord
	err := db.Conn.QueryRow(ctx, `
		SELECT id, booking_id, payment_type, payment_method, vendor_code, invoice_number,
		       status, amount, is_vat_applicable, vat_rate, vat_amount, net_amount,
		       user_saved_card_id, square_payment_id, idempotency_key, fees, created_at, updated_at, created_by,
		       gift_card_id
		FROM payments
		WHERE id = $1
	`, paymentID).Scan(
		&p.ID, &p.BookingID, &p.PaymentType, &p.PaymentMethod, &p.VendorCode, &p.InvoiceNumber,
		&p.Status, &p.Amount, &p.IsVATApplicable, &p.VATRate, &p.VATAmount, &p.NetAmount,
		&p.UserSavedCardID, &p.SquarePaymentID, &p.IdempotencyKey, &p.Fees,
		&p.CreatedAt, &p.UpdatedAt, &p.CreatedBy, &p.GiftCardID,
	)

	if err != nil {
		return nil, err
	}
	return &p, nil
}

func (s *PaymentService) GetAlreadyRefundedAmount(ctx context.Context, paymentID string) (int64, error) {
	var amount float64
	err := db.Conn.QueryRow(ctx, `
		SELECT COALESCE(SUM(amount), 0) FROM refunds
		WHERE payment_id = $1 AND status = 'completed'
	`, paymentID).Scan(&amount)

	if err != nil {
		return 0, err
	}
	return int64(amount * 100), nil
}

func (s *PaymentService) HasCompletedPayment(ctx context.Context, bookingID string) (bool, error) {
	var count int
	err := db.Conn.QueryRow(ctx, `
		SELECT COUNT(*) FROM payments
		WHERE booking_id = $1 AND status = 'completed' AND payment_type IN ('full', 'deposit', 'balance', 'partial')
	`, bookingID).Scan(&count)
	if err != nil {
		return false, err
	}
	return count > 0, nil
}

func (s *PaymentService) GetBookingStatus(ctx context.Context, bookingID string) (string, error) {
	var status string
	err := db.Conn.QueryRow(ctx, `SELECT status FROM bookings WHERE id = $1`, bookingID).Scan(&status)
	if err != nil {
		return "", err
	}
	return status, nil
}

// BookingPaymentInfo holds booking-level data needed for payment split decisions.
type BookingPaymentInfo struct {
	StartTime   time.Time
	TotalAmount float64
	TotalPaid   float64
	Status      string
}

// GetBookingPaymentInfo fetches the booking start time, total service amount, and
// total completed payments for a booking.
func (s *PaymentService) GetBookingPaymentInfo(ctx context.Context, bookingID string) (*BookingPaymentInfo, error) {
	var info BookingPaymentInfo
	err := db.Conn.QueryRow(ctx, `
		SELECT b.start_time, b.status,
			COALESCE(b.total_amount, 0),
			COALESCE(pt.total_paid, 0)
		FROM bookings b
		LEFT JOIN (
			SELECT booking_id, SUM(amount) AS total_paid
			FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house') GROUP BY booking_id
		) pt ON b.id = pt.booking_id
		WHERE b.id = $1
	`, bookingID).Scan(&info.StartTime, &info.Status, &info.TotalAmount, &info.TotalPaid)
	if err != nil {
		return nil, err
	}
	return &info, nil
}

func (s *PaymentService) GetBookingUserID(ctx context.Context, bookingID string) (string, error) {
	var userID string
	err := db.Conn.QueryRow(ctx, `SELECT user_id FROM bookings WHERE id = $1`, bookingID).Scan(&userID)
	if err != nil {
		return "", err
	}
	return userID, nil
}

func (s *PaymentService) GetBookingRemainingBalanceCents(ctx context.Context, bookingID string) (int64, error) {
	var remainingCents int64
	err := db.Conn.QueryRow(ctx, `
	WITH booking_total AS (
		SELECT total_amount AS total_pounds FROM bookings WHERE id = $1
	),
		paid_total AS (
			SELECT COALESCE(SUM(amount), 0) AS paid_pounds
			FROM payments
			WHERE booking_id = $1 AND status = 'completed'
		)
		SELECT GREATEST(0, ROUND((bt.total_pounds - pt.paid_pounds) * 100))::bigint
		FROM booking_total bt, paid_total pt
	`, bookingID).Scan(&remainingCents)
	if err != nil {
		return 0, err
	}
	return remainingCents, nil
}

func (s *PaymentService) GetUserPaymentMethods(ctx context.Context, userID string) ([]SavedCard, error) {
	rows, err := db.Conn.Query(ctx, `
		SELECT id, square_card_id, brand, last_4, exp_month, exp_year, fingerprint, is_default
		FROM user_saved_cards
		WHERE user_id = $1 AND deleted_at IS NULL
		ORDER BY is_default DESC, created_at DESC
	`, userID)

	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var cards []SavedCard
	for rows.Next() {
		var c SavedCard
		err := rows.Scan(&c.ID, &c.SquareCardID, &c.Brand, &c.Last4, &c.ExpMonth, &c.ExpYear, &c.Fingerprint, &c.IsDefault)
		if err != nil {
			return nil, err
		}
		cards = append(cards, c)
	}

	if cards == nil {
		cards = []SavedCard{}
	}

	return cards, nil
}

func (s *PaymentService) DeletePaymentMethod(ctx context.Context, cardID, userID string) error {
	tx, err := db.Conn.Begin(ctx)
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		return err
	}
	defer func() {
    if err := tx.Rollback(ctx); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	retainedUntil := clock.Now().Add(7 * 365 * 24 * time.Hour)
	_, err = tx.Exec(ctx, `
		UPDATE user_saved_cards
		SET deleted_at = NOW(), deleted_by = $1, retained_until = $2
		WHERE id = $3 AND user_id = $1
	`, userID, retainedUntil, cardID)
	if err != nil {
		return err
	}

	if err := tx.Commit(ctx); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		return err
	}

	return nil
}

func (s *PaymentService) CreatePaymentMethodFromDetails(ctx context.Context, userID, cardNumber, expiry, cvc string) (*SavedCard, error) {
	parts := strings.Split(expiry, "/")
	if len(parts) != 2 {
		return nil, errors.New("invalid expiry format, use MM/YY")
	}
	expMonth, err := strconv.Atoi(parts[0])
	if err != nil || expMonth < 1 || expMonth > 12 {
		return nil, errors.New("invalid expiry month")
	}
	expYear, err := strconv.Atoi(parts[1])
	if err != nil || expYear < 0 || expYear > 99 {
		return nil, errors.New("invalid expiry year")
	}
	expYear += 2000

	// Check if card is expired
	now := clock.Now()
	expiryDate := time.Date(expYear, time.Month(expMonth), 1, 0, 0, 0, 0, time.UTC)
	if expiryDate.Before(now) {
		return nil, errors.New("card has expired")
	}

	cardOnFile, err := SquareClient.CreateCardOnFileRaw(ctx, userID, cardNumber, expMonth, expYear, cvc)
	if err != nil {
		return nil, fmt.Errorf("failed to tokenize card: %w", err)
	}

	var savedCardID string
	var isDefault bool
	err = db.Conn.QueryRow(ctx, `
		INSERT INTO user_saved_cards (user_id, square_card_id, brand, last_4, exp_month, exp_year, fingerprint, is_default)
		SELECT $1, $2, $3, $4, $5, $6, $7,
			NOT EXISTS(SELECT 1 FROM user_saved_cards WHERE user_id = $1 AND deleted_at IS NULL)
		RETURNING id, is_default
	`, userID, cardOnFile.CardID, cardOnFile.Brand, cardOnFile.Last4, expMonth, expYear, cardOnFile.Fingerprint).Scan(&savedCardID, &isDefault)
	if err != nil {
		return nil, fmt.Errorf("failed to save card: %w", err)
	}

	return &SavedCard{
		ID:           savedCardID,
		SquareCardID: cardOnFile.CardID,
		Brand:        cardOnFile.Brand,
		Last4:        cardOnFile.Last4,
		ExpMonth:     expMonth,
		ExpYear:      expYear,
		Fingerprint:  cardOnFile.Fingerprint,
		IsDefault:    isDefault,
	}, nil
}

func (s *PaymentService) SaveCardForUser(ctx context.Context, userID, squareCardID, brand, last4 string, expMonth, expYear int, fingerprint string) (string, error) {
	var id string
	err := db.Conn.QueryRow(ctx, `
		INSERT INTO user_saved_cards (
			user_id, square_card_id, brand, last_4, exp_month, exp_year, fingerprint, is_default, created_at
		) VALUES ($1, $2, $3, $4, $5, $6, $7, false, NOW())
		RETURNING id
	`, userID, squareCardID, brand, last4, expMonth, expYear, fingerprint).Scan(&id)

	if err != nil {
		return "", err
	}
	return id, nil
}

func (s *PaymentService) GetCardByID(ctx context.Context, cardID, userID string) (*SavedCard, error) {
	return s.GetCardByIDQuerier(ctx, db.Conn, cardID, userID)
}

// GetCardByIDQuerier is identical to GetCardByID but accepts a db.Querier
// so the lookup can be performed inside a transaction. Callers inside an
// existing transaction should pass their tx variable instead of db.Conn.
func (s *PaymentService) GetCardByIDQuerier(ctx context.Context, q db.Querier, cardID, userID string) (*SavedCard, error) {
	var c SavedCard
	err := q.QueryRow(ctx, `
		SELECT id, square_card_id, brand, last_4, exp_month, exp_year, fingerprint, is_default
		FROM user_saved_cards
		WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL
	`, cardID, userID).Scan(&c.ID, &c.SquareCardID, &c.Brand, &c.Last4, &c.ExpMonth, &c.ExpYear, &c.Fingerprint, &c.IsDefault)

	if err != nil {
		return nil, err
	}
	return &c, nil
}

var SquareClient square.SquareClient
