package main

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"strings"
	"time"

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

	"github.com/niels/banking-app/backend/internal/config"
	"github.com/niels/banking-app/backend/internal/integrations/bankledger"
	"github.com/niels/banking-app/backend/internal/ledger"
	"github.com/niels/banking-app/backend/internal/platform/database"
	"github.com/niels/banking-app/backend/internal/security"
)

const (
	defaultAdminEmail    = "admin.staging@swiftnet.test"
	defaultCustomerEmail = "customer.staging@swiftnet.test"
	defaultSeedPassword  = "very-secure-passphrase"
)

type seedUser struct {
	ID       string
	Email    string
	FullName string
	Role     string
	Password string
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	cfg, err := config.Load()
	if err != nil {
		slog.Error("configuration error", "error", err)
		os.Exit(1)
	}
	if cfg.App.Environment == "production" {
		slog.Error("refusing to seed production")
		os.Exit(1)
	}
	if !envBool("BANKING_ALLOW_STAGING_SEED") {
		slog.Error("refusing to seed without BANKING_ALLOW_STAGING_SEED=true")
		os.Exit(1)
	}

	db, err := database.Connect(ctx, cfg.Database)
	if err != nil {
		slog.Error("database connection failed", "error", err)
		os.Exit(1)
	}
	defer db.Close()

	admin, err := upsertUser(ctx, db, seedUser{
		Email:    env("BANKING_STAGING_ADMIN_EMAIL", defaultAdminEmail),
		FullName: "SwiftNet Staging Admin",
		Role:     "admin",
		Password: env("BANKING_STAGING_ADMIN_PASSWORD", defaultSeedPassword),
	})
	if err != nil {
		slog.Error("admin seed failed", "error", err)
		os.Exit(1)
	}

	customer, err := upsertUser(ctx, db, seedUser{
		Email:    env("BANKING_STAGING_CUSTOMER_EMAIL", defaultCustomerEmail),
		FullName: "SwiftNet Staging Customer",
		Role:     "customer",
		Password: env("BANKING_STAGING_CUSTOMER_PASSWORD", defaultSeedPassword),
	})
	if err != nil {
		slog.Error("customer seed failed", "error", err)
		os.Exit(1)
	}
	if err := seedCustomerWorkspace(ctx, db, customer.ID); err != nil {
		slog.Error("customer workspace seed failed", "error", err)
		os.Exit(1)
	}

	fmt.Printf("staging seed ready: admin=%s customer=%s\n", admin.Email, customer.Email)
}

func upsertUser(ctx context.Context, db *pgxpool.Pool, user seedUser) (seedUser, error) {
	passwordHash, err := security.HashPassword(user.Password)
	if err != nil {
		return seedUser{}, err
	}
	row := db.QueryRow(ctx, `
		INSERT INTO users (email, full_name, password_hash, role)
		VALUES (lower($1), $2, $3, $4)
		ON CONFLICT (lower(email)) DO UPDATE
		SET full_name = EXCLUDED.full_name,
			password_hash = EXCLUDED.password_hash,
			role = EXCLUDED.role
		RETURNING id::text, email, full_name, role
	`, user.Email, user.FullName, passwordHash, user.Role)
	if err := row.Scan(&user.ID, &user.Email, &user.FullName, &user.Role); err != nil {
		return seedUser{}, err
	}
	if err := upsertVerifiedKYC(ctx, db, user); err != nil {
		return seedUser{}, err
	}
	if err := insertClearAMLScreening(ctx, db, user.ID); err != nil {
		return seedUser{}, err
	}
	return user, nil
}

func upsertVerifiedKYC(ctx context.Context, db *pgxpool.Pool, user seedUser) error {
	_, err := db.Exec(ctx, `
		INSERT INTO kyc_profiles (
			user_id, legal_name, date_of_birth, country, address_line1, city, postal_code,
			status, provider, external_verification_id, submitted_at, verified_at
		)
		VALUES (
			$1, $2, '1990-01-01', 'NL', 'Stagingstraat 1', 'Amsterdam', '1000AA',
			'verified', 'local_identity', $3, now(), now()
		)
		ON CONFLICT (user_id) DO UPDATE
		SET legal_name = EXCLUDED.legal_name,
			status = 'verified',
			provider = EXCLUDED.provider,
			external_verification_id = EXCLUDED.external_verification_id,
			submitted_at = COALESCE(kyc_profiles.submitted_at, now()),
			verified_at = now(),
			rejection_reason = NULL
	`, user.ID, user.FullName, "staging-kyc-"+user.Role)
	return err
}

func insertClearAMLScreening(ctx context.Context, db *pgxpool.Pool, userID string) error {
	_, err := db.Exec(ctx, `
		INSERT INTO aml_screenings (
			user_id, kyc_profile_id, screening_type, provider, status, risk_score,
			matched, external_screening_id
		)
		SELECT $1, kp.id, 'initial', 'local_sanctions', 'clear', 1, false, 'staging-aml-' || $1
		FROM kyc_profiles kp
		WHERE kp.user_id = $1
			AND NOT EXISTS (
				SELECT 1 FROM aml_screenings existing
				WHERE existing.external_screening_id = 'staging-aml-' || $1
			)
	`, userID)
	return err
}

func seedCustomerWorkspace(ctx context.Context, db *pgxpool.Pool, userID string) error {
	tx, err := db.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx)

	walletID, err := ensureWallet(ctx, tx, userID)
	if err != nil {
		return err
	}
	accountID, err := ensureAccount(ctx, tx, userID, walletID, "EUR", "9000000001")
	if err != nil {
		return err
	}
	if err := ensureOpeningBalance(ctx, tx, userID, walletID, accountID, "EUR", 250_000); err != nil {
		return err
	}
	if err := ensureSavingsGoal(ctx, tx, userID, accountID); err != nil {
		return err
	}
	if err := ensureCryptoWallet(ctx, tx, userID); err != nil {
		return err
	}
	return tx.Commit(ctx)
}

func ensureWallet(ctx context.Context, tx pgx.Tx, userID string) (string, error) {
	var walletID string
	err := tx.QueryRow(ctx, `
		WITH existing AS (
			SELECT id FROM wallets WHERE user_id = $1 AND name = 'Staging wallet' LIMIT 1
		), inserted AS (
			INSERT INTO wallets (user_id, name)
			SELECT $1, 'Staging wallet'
			WHERE NOT EXISTS (SELECT 1 FROM existing)
			RETURNING id
		)
		SELECT id::text FROM inserted
		UNION ALL
		SELECT id::text FROM existing
		LIMIT 1
	`, userID).Scan(&walletID)
	return walletID, err
}

func ensureAccount(ctx context.Context, tx pgx.Tx, userID, walletID, currency, accountNumber string) (string, error) {
	iban, err := bankledger.GenerateDutchIBAN(accountNumber)
	if err != nil {
		return "", err
	}
	var accountID string
	err = tx.QueryRow(ctx, `
		INSERT INTO accounts (
			user_id, wallet_id, account_number, iban, bic, bank_provider, external_account_id, currency, status
		)
		VALUES ($1, $2, $3, $4, $5, 'local_bank_ledger', $6, $7, 'active')
		ON CONFLICT (external_account_id) DO UPDATE
		SET wallet_id = EXCLUDED.wallet_id,
			status = 'active'
		RETURNING id::text
	`, userID, walletID, accountNumber, iban, bankledger.DefaultBIC, "staging-account-"+currency, currency).Scan(&accountID)
	if err != nil {
		return "", err
	}
	_, err = tx.Exec(ctx, `
		INSERT INTO account_routing_codes (account_id, code_type, country, network, code)
		VALUES ($1, 'bank_code', 'NL', 'sepa', $2)
		ON CONFLICT (account_id, country, network, code_type, code) DO UPDATE
		SET status = 'active',
			updated_at = now()
	`, accountID, bankledger.DefaultBankCode)
	return accountID, err
}

func ensureOpeningBalance(ctx context.Context, tx pgx.Tx, userID, walletID, accountID, currency string, amountCents int64) error {
	_, err := tx.Exec(ctx, `
		INSERT INTO wallet_balances (wallet_id, currency, available_balance_cents)
		VALUES ($1, $2, $3)
		ON CONFLICT (wallet_id, currency) DO UPDATE
		SET available_balance_cents = EXCLUDED.available_balance_cents,
			updated_at = now()
	`, walletID, currency, amountCents)
	if err != nil {
		return err
	}
	if _, err := tx.Exec(ctx, `
		UPDATE accounts
		SET balance_cents = $2,
			updated_at = now()
		WHERE id = $1
	`, accountID, amountCents); err != nil {
		return err
	}

	ledgerRepo := ledger.NewRepository(nil)
	accountLedger, err := ledgerRepo.EnsureAccount(ctx, tx, ledger.AccountParams{
		OwnerUserID:   userID,
		ReferenceType: "account",
		ReferenceID:   accountID,
		Currency:      currency,
		NormalBalance: "credit",
	})
	if err != nil {
		return err
	}
	seedCash, err := ledgerRepo.EnsureAccount(ctx, tx, ledger.AccountParams{
		ReferenceType: "staging_seed_cash",
		ReferenceID:   "opening_balance",
		Currency:      currency,
		NormalBalance: "debit",
	})
	if err != nil {
		return err
	}
	_, err = ledgerRepo.Post(ctx, tx, ledger.PostParams{
		EventType:      "staging.opening_balance",
		SourceType:     "staging_seed",
		SourceID:       "opening-balance-" + strings.ToLower(currency),
		IdempotencyKey: "staging-opening-balance-" + strings.ToLower(currency),
		Description:    "Staging opening balance",
		Metadata: map[string]any{
			"user_id":    userID,
			"wallet_id":  walletID,
			"account_id": accountID,
		},
		Lines: []ledger.LineParams{
			{LedgerAccountID: accountLedger.ID, Direction: "credit", AmountCents: amountCents, Currency: currency},
			{LedgerAccountID: seedCash.ID, Direction: "debit", AmountCents: amountCents, Currency: currency},
		},
	})
	return err
}

func ensureSavingsGoal(ctx context.Context, tx pgx.Tx, userID, accountID string) error {
	_, err := tx.Exec(ctx, `
		INSERT INTO savings_goals (user_id, account_id, name, currency, target_amount_cents, target_date)
		SELECT $1, $2, 'Emergency fund', 'EUR', 500000, CURRENT_DATE + INTERVAL '180 days'
		WHERE NOT EXISTS (
			SELECT 1 FROM savings_goals
			WHERE user_id = $1 AND account_id = $2 AND name = 'Emergency fund'
		)
	`, userID, accountID)
	return err
}

func ensureCryptoWallet(ctx context.Context, tx pgx.Tx, userID string) error {
	var walletID string
	if err := tx.QueryRow(ctx, `
		INSERT INTO crypto_wallets (user_id, name, custody_provider, external_wallet_id)
		VALUES ($1, 'Staging crypto wallet', 'local_custody', 'staging-crypto-wallet-' || $1)
		ON CONFLICT (external_wallet_id) DO UPDATE
		SET status = 'active',
			updated_at = now()
		RETURNING id::text
	`, userID).Scan(&walletID); err != nil {
		return err
	}
	_, err := tx.Exec(ctx, `
		INSERT INTO crypto_wallet_balances (crypto_wallet_id, crypto_asset_id, available_amount_base_units)
		SELECT $1, ca.id, 100000000
		FROM crypto_assets ca
		WHERE ca.symbol = 'USDC' AND ca.network = 'ethereum'
		ON CONFLICT (crypto_wallet_id, crypto_asset_id) DO UPDATE
		SET available_amount_base_units = EXCLUDED.available_amount_base_units,
			updated_at = now()
	`, walletID)
	return err
}

func env(key, fallback string) string {
	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return fallback
	}
	return value
}

func envBool(key string) bool {
	switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) {
	case "1", "true", "yes", "y", "on":
		return true
	default:
		return false
	}
}
