package main

import (
	"database/sql"
	"encoding/json"
	"encoding/xml"
	"html/template"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/google/uuid"
	_ "github.com/jackc/pgx/v5/stdlib"
	amqp "github.com/rabbitmq/amqp091-go"
	"golang.org/x/crypto/bcrypt"
)

type license struct {
	ID     string `json:"id"`
	Price  int    `json:"price"`
	Name   string `json:"name"`
	Hidden bool   `json:"hidden"`
}

type purchaseRequest struct {
	LicenseID string `json:"license_id"`
	Amount    int    `json:"amount"`
}

type xmlPurchaseRequest struct {
	XMLName   xml.Name `xml:"purchase"`
	LicenseID string   `xml:"license_id"`
	Amount    int      `xml:"amount"`
}

type paymentMessage struct {
	PaymentID string    `json:"payment_id"`
	UserID    int       `json:"user_id"`
	LicenseID string    `json:"license_id"`
	Amount    int       `json:"amount"`
	Timestamp time.Time `json:"timestamp"`
}

type xmlPayment struct {
	XMLName   xml.Name `xml:"payment"`
	PaymentID string   `xml:"payment_id"`
	UserID    int      `xml:"user_id"`
	LicenseID string   `xml:"license_id"`
	Amount    int      `xml:"amount"`
	Timestamp string   `xml:"timestamp"`
}

type authPayload struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type sessionInfo struct {
	UserID   int
	Username string
}

func main() {
	port := envDefault("PORT", "8080")
	rabbitURL := envDefault("RABBITMQ_URL", "amqp://guest:guest@rabbitmq:5672/")
	dbDSN := envDefault("DB_DSN", "postgres://license:license@postgres:5432/licenses?sslmode=disable")
	flag := envDefault("FLAG", "vsosh{race_condition_is_still_a_thing_in_2026}")
	secretFlag := envDefault("SECRET_FLAG", "vsosh{secret_path_flag}")

	db := mustOpenDB(dbDSN)
	defer db.Close()

	if err := initSchema(db); err != nil {
		log.Fatalf("schema: %v", err)
	}

	conn := mustDialRabbit(rabbitURL)
	defer conn.Close()

	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("amqp channel: %v", err)
	}
	defer ch.Close()

	declareQueue(ch, "payments")
	declareQueue(ch, "payments-xml")

	tmpl := template.Must(template.ParseFiles("templates/index.html"))

	mux := http.NewServeMux()
	mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/" {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		if err := tmpl.Execute(w, nil); err != nil {
			log.Printf("template: %v", err)
		}
	})

	mux.HandleFunc("/api/balance", func(w http.ResponseWriter, r *http.Request) {
		info, err := getSessionInfo(db, r)
		if err != nil {
			writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "auth"})
			return
		}
		balance, err := getBalance(db, info.UserID)
		if err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
			return
		}
		writeJSON(w, http.StatusOK, map[string]interface{}{"balance": balance, "currency": "RUB"})
	})

	mux.HandleFunc("/api/licenses", func(w http.ResponseWriter, r *http.Request) {
		licenses, err := getLicenses(db)
		if err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
			return
		}
		writeJSON(w, http.StatusOK, licenses)
	})

	mux.HandleFunc("/api/me", func(w http.ResponseWriter, r *http.Request) {
		info, err := getSessionInfo(db, r)
		if err != nil {
			writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "auth"})
			return
		}
		balance, err := getBalance(db, info.UserID)
		if err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
			return
		}
		writeJSON(w, http.StatusOK, map[string]interface{}{
			"username": info.Username,
			"balance":  balance,
			"currency": "RUB",
		})
	})

	mux.HandleFunc("/api/register", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		var payload authPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad json"})
			return
		}
		if payload.Username == "" || payload.Password == "" {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing"})
			return
		}
		userID, err := registerUser(db, payload.Username, payload.Password)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "user exists"})
			return
		}
		if err := createSession(db, w, userID); err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "session"})
			return
		}
		writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
	})

	mux.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		var payload authPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad json"})
			return
		}
		if payload.Username == "" || payload.Password == "" {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing"})
			return
		}
		userID, err := authenticateUser(db, payload.Username, payload.Password)
		if err != nil {
			writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid"})
			return
		}
		if err := createSession(db, w, userID); err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "session"})
			return
		}
		writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
	})

	mux.HandleFunc("/api/logout", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		if err := clearSession(db, w, r); err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "logout"})
			return
		}
		writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
	})

	mux.HandleFunc("/api/purchase", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		info, err := getSessionInfo(db, r)
		if err != nil {
			writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "auth"})
			return
		}
		var req purchaseRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad json"})
			return
		}
		msg := paymentMessage{
			PaymentID: uuid.NewString(),
			UserID:    info.UserID,
			LicenseID: req.LicenseID,
			Amount:    req.Amount,
			Timestamp: time.Now().UTC(),
		}
		if err := createPayment(db, msg); err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
			return
		}
		payload, _ := json.Marshal(msg)
		if err := ch.Publish("", "payments", false, false, amqp.Publishing{
			ContentType:  "application/json",
			Body:         payload,
			DeliveryMode: amqp.Persistent,
		}); err != nil {
			writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "queue"})
			return
		}
		writeJSON(w, http.StatusOK, map[string]string{"status": "pending", "payment_id": msg.PaymentID})
	})

	mux.HandleFunc("/api/purchase-xml", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		info, err := getSessionInfo(db, r)
		if err != nil {
			writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "auth"})
			return
		}
		var req xmlPurchaseRequest
		if err := xml.NewDecoder(r.Body).Decode(&req); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad xml"})
			return
		}
		xmlMsg := xmlPayment{
			PaymentID: uuid.NewString(),
			UserID:    info.UserID,
			LicenseID: req.LicenseID,
			Amount:    req.Amount,
			Timestamp: time.Now().UTC().Format(time.RFC3339),
		}
		if err := createPayment(db, paymentMessage{
			PaymentID: xmlMsg.PaymentID,
			UserID:    xmlMsg.UserID,
			LicenseID: xmlMsg.LicenseID,
			Amount:    xmlMsg.Amount,
			Timestamp: time.Now().UTC(),
		}); err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
			return
		}
		payload, _ := xml.Marshal(xmlMsg)
		if err := ch.Publish("", "payments-xml", false, false, amqp.Publishing{
			ContentType:  "application/xml",
			Body:         payload,
			DeliveryMode: amqp.Persistent,
		}); err != nil {
			writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "queue"})
			return
		}
		writeJSON(w, http.StatusOK, map[string]string{"status": "pending", "payment_id": xmlMsg.PaymentID})
	})

	mux.HandleFunc("/api/my-licenses", func(w http.ResponseWriter, r *http.Request) {
		info, err := getSessionInfo(db, r)
		if err != nil {
			writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "auth"})
			return
		}
		licenses, err := getUserLicenses(db, info.UserID)
		if err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
			return
		}
		for i := range licenses {
			if licenses[i]["id"] == "license-3" {
				licenses[i]["flag"] = flag
			}
		}
		writeJSON(w, http.StatusOK, licenses)
	})

	mux.HandleFunc("/secret", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		_, _ = w.Write([]byte(secretFlag))
	})

	mux.HandleFunc("/api/purchases", func(w http.ResponseWriter, r *http.Request) {
		info, err := getSessionInfo(db, r)
		if err != nil {
			writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "auth"})
			return
		}
		purchases, err := getUserPayments(db, info.UserID)
		if err != nil {
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
			return
		}
		writeJSON(w, http.StatusOK, purchases)
	})

	server := &http.Server{
		Addr:              ":" + port,
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}

	log.Printf("license shop running on :%s", port)
	if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatalf("server: %v", err)
	}
}

func envDefault(key, fallback string) string {
	val := os.Getenv(key)
	if val == "" {
		return fallback
	}
	return val
}

func writeJSON(w http.ResponseWriter, status int, payload interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(payload)
}

func mustOpenDB(dsn string) *sql.DB {
	var lastErr error
	for i := 0; i < 30; i++ {
		db, err := sql.Open("pgx", dsn)
		if err != nil {
			lastErr = err
			time.Sleep(2 * time.Second)
			continue
		}
		if err := db.Ping(); err != nil {
			lastErr = err
			_ = db.Close()
			time.Sleep(2 * time.Second)
			continue
		}
		return db
	}
	log.Fatalf("db: %v", lastErr)
	return nil
}

func initSchema(db *sql.DB) error {
	queries := []string{
		`CREATE TABLE IF NOT EXISTS users (
			id INTEGER PRIMARY KEY,
			username TEXT UNIQUE,
			password_hash TEXT,
			balance INTEGER NOT NULL
		)`,
		`CREATE TABLE IF NOT EXISTS licenses (id TEXT PRIMARY KEY, name TEXT NOT NULL, price INTEGER NOT NULL, hidden BOOLEAN NOT NULL)`,
		`CREATE TABLE IF NOT EXISTS user_licenses (user_id INTEGER NOT NULL, license_id TEXT NOT NULL, PRIMARY KEY (user_id, license_id))`,
		`CREATE TABLE IF NOT EXISTS user_payments (id BIGSERIAL PRIMARY KEY, user_id INTEGER NOT NULL, license_id TEXT NOT NULL, amount INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL)`,
		`CREATE TABLE IF NOT EXISTS payments (
			payment_id TEXT PRIMARY KEY,
			user_id INTEGER NOT NULL,
			license_id TEXT NOT NULL,
			amount INTEGER NOT NULL,
			status TEXT NOT NULL,
			created_at TIMESTAMPTZ NOT NULL,
			processed_at TIMESTAMPTZ
		)`,
		`CREATE TABLE IF NOT EXISTS sessions (token TEXT PRIMARY KEY, user_id INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL)`,
		`CREATE SEQUENCE IF NOT EXISTS users_id_seq`,
		`ALTER TABLE users ALTER COLUMN id SET DEFAULT nextval('users_id_seq')`,
	}
	for _, q := range queries {
		if _, err := db.Exec(q); err != nil {
			return err
		}
	}
	if _, err := db.Exec(`SELECT setval('users_id_seq', COALESCE((SELECT MAX(id) FROM users), 0) + 1, false)`); err != nil {
		return err
	}
	if _, err := db.Exec(`INSERT INTO users (id, balance) VALUES (1, 100) ON CONFLICT (id) DO NOTHING`); err != nil {
		return err
	}
	seeds := []license{
		{ID: "license-1", Name: "Полная лицензий", Price: 100, Hidden: false},
		{ID: "license-2", Name: "Без смены ника", Price: 130, Hidden: false},
		{ID: "license-3", Name: "Полная с флагом на плаще", Price: 200, Hidden: false},
		{ID: "license-4", Name: "Без гарантии", Price: 70, Hidden: false},
		{ID: "license-5", Name: "Без смены скина", Price: 90, Hidden: false},
		{ID: "license-6", Name: "Доступ на сервера СП", Price: 160, Hidden: false},
	}
	for _, lic := range seeds {
		_, err := db.Exec(
			`INSERT INTO licenses (id, name, price, hidden) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO NOTHING`,
			lic.ID, lic.Name, lic.Price, lic.Hidden,
		)
		if err != nil {
			return err
		}
	}
	return nil
}

func getBalance(db *sql.DB, userID int) (int, error) {
	var balance int
	err := db.QueryRow(`SELECT balance FROM users WHERE id = $1`, userID).Scan(&balance)
	return balance, err
}

func registerUser(db *sql.DB, username, password string) (int, error) {
	hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return 0, err
	}
	var userID int
	err = db.QueryRow(
		`INSERT INTO users (username, password_hash, balance) VALUES ($1, $2, 100) RETURNING id`,
		username, string(hashed),
	).Scan(&userID)
	return userID, err
}

func authenticateUser(db *sql.DB, username, password string) (int, error) {
	var (
		userID int
		hash   string
	)
	err := db.QueryRow(`SELECT id, password_hash FROM users WHERE username = $1`, username).Scan(&userID, &hash)
	if err != nil {
		return 0, err
	}
	if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil {
		return 0, err
	}
	return userID, nil
}

func createSession(db *sql.DB, w http.ResponseWriter, userID int) error {
	token := uuid.NewString()
	if _, err := db.Exec(
		`INSERT INTO sessions (token, user_id, created_at) VALUES ($1, $2, $3)`,
		token, userID, time.Now().UTC(),
	); err != nil {
		return err
	}
	http.SetCookie(w, &http.Cookie{
		Name:     "session",
		Value:    token,
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	})
	return nil
}

func clearSession(db *sql.DB, w http.ResponseWriter, r *http.Request) error {
	cookie, err := r.Cookie("session")
	if err == nil {
		_, _ = db.Exec(`DELETE FROM sessions WHERE token = $1`, cookie.Value)
	}
	http.SetCookie(w, &http.Cookie{
		Name:     "session",
		Value:    "",
		Path:     "/",
		MaxAge:   -1,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	})
	return nil
}

func getSessionInfo(db *sql.DB, r *http.Request) (sessionInfo, error) {
	cookie, err := r.Cookie("session")
	if err != nil || cookie.Value == "" {
		return sessionInfo{}, err
	}
	var info sessionInfo
	err = db.QueryRow(
		`SELECT u.id, u.username FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token = $1`,
		cookie.Value,
	).Scan(&info.UserID, &info.Username)
	return info, err
}

func getLicenses(db *sql.DB) ([]license, error) {
	rows, err := db.Query(`SELECT id, name, price, hidden FROM licenses`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var result []license
	for rows.Next() {
		var lic license
		if err := rows.Scan(&lic.ID, &lic.Name, &lic.Price, &lic.Hidden); err != nil {
			return nil, err
		}
		result = append(result, lic)
	}
	return result, rows.Err()
}

func getUserLicenses(db *sql.DB, userID int) ([]map[string]string, error) {
	result := make([]map[string]string, 0)
	rows, err := db.Query(
		`SELECT l.id, l.name FROM user_licenses ul JOIN licenses l ON l.id = ul.license_id WHERE ul.user_id = $1`,
		userID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var id, name string
		if err := rows.Scan(&id, &name); err != nil {
			return nil, err
		}
		result = append(result, map[string]string{"id": id, "name": name})
	}
	return result, rows.Err()
}

func createPayment(db *sql.DB, msg paymentMessage) error {
	_, err := db.Exec(
		`INSERT INTO payments (payment_id, user_id, license_id, amount, status, created_at) VALUES ($1, $2, $3, $4, $5, $6)`,
		msg.PaymentID, msg.UserID, msg.LicenseID, msg.Amount, "pending", msg.Timestamp,
	)
	return err
}

func getUserPayments(db *sql.DB, userID int) ([]map[string]interface{}, error) {
	rows, err := db.Query(
		`SELECT p.payment_id, p.license_id, l.name, p.amount, p.status, p.created_at, p.processed_at
		 FROM payments p
		 JOIN licenses l ON l.id = p.license_id
		 WHERE p.user_id = $1
		 ORDER BY p.created_at DESC
		 LIMIT 20`,
		userID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	result := make([]map[string]interface{}, 0)
	for rows.Next() {
		var (
			paymentID   string
			licenseID   string
			name        string
			amount      int
			status      string
			createdAt   time.Time
			processedAt sql.NullTime
		)
		if err := rows.Scan(&paymentID, &licenseID, &name, &amount, &status, &createdAt, &processedAt); err != nil {
			return nil, err
		}
		item := map[string]interface{}{
			"id":         paymentID,
			"license_id": licenseID,
			"name":       name,
			"amount":     amount,
			"status":     status,
			"created_at": createdAt.Format(time.RFC3339),
		}
		if processedAt.Valid {
			item["processed_at"] = processedAt.Time.Format(time.RFC3339)
		}
		result = append(result, item)
	}
	return result, rows.Err()
}

func mustDialRabbit(url string) *amqp.Connection {
	var lastErr error
	for i := 0; i < 30; i++ {
		conn, err := amqp.Dial(url)
		if err == nil {
			return conn
		}
		lastErr = err
		time.Sleep(2 * time.Second)
	}
	log.Fatalf("rabbitmq: %v", lastErr)
	return nil
}

func declareQueue(ch *amqp.Channel, name string) {
	_, err := ch.QueueDeclare(
		name,
		true,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		log.Fatalf("queue %s: %v", name, err)
	}
}
