package main

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	_ "github.com/lib/pq"
)

type App struct {
	db         *sql.DB
	auth       *Auth
	templates  *template.Template
	storageDir string
	devMode    bool
}

type Skill struct {
	ID          int64
	OwnerSub    string
	OwnerEmail  string
	Title       string
	Description string
	PriceCents  int
	FilePath    string
	CreatedAt   time.Time
}

type UserSkill struct {
	SkillID     int64
	Title       string
	Description string
	OwnerEmail  string
	PurchasedAt time.Time
}

func main() {
	cfg, err := LoadConfig()
	if err != nil {
		log.Fatalf("load config: %v", err)
	}

	db, err := sql.Open("postgres", cfg.DatabaseURL)
	if err != nil {
		log.Fatalf("open database: %v", err)
	}
	defer db.Close()

	if err := db.Ping(); err != nil {
		log.Fatalf("ping database: %v", err)
	}

	if err := runMigrations(db); err != nil {
		log.Fatalf("run migrations: %v", err)
	}

	auth, err := NewAuth(cfg)
	if err != nil {
		log.Fatalf("init auth: %v", err)
	}

	if err := os.MkdirAll(cfg.StorageDir, 0o755); err != nil {
		log.Fatalf("create storage dir: %v", err)
	}

	tmpl, err := template.ParseGlob("templates/*.html")
	if err != nil {
		log.Fatalf("parse templates: %v", err)
	}

	app := &App{
		db:         db,
		auth:       auth,
		templates:  tmpl,
		storageDir: cfg.StorageDir,
		devMode:    cfg.DevMode,
	}
	app.auth.WithDB(db)

	mux := http.NewServeMux()
	mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
	mux.Handle("/uploads/", app.auth.RequireAuth(http.HandlerFunc(app.handleDownload)))
	mux.HandleFunc("/", app.handleHome)
	mux.Handle("/dashboard", app.auth.RequireAuth(http.HandlerFunc(app.handleDashboard)))
	mux.Handle("/skills", app.auth.RequireAuth(http.HandlerFunc(app.handleSkills)))
	mux.Handle("/purchase", app.auth.RequireAuth(http.HandlerFunc(app.handlePurchase)))
	mux.Handle("/my-skills", app.auth.RequireAuth(http.HandlerFunc(app.handleMySkills)))
	mux.Handle("/api/me", app.auth.RequireAuth(http.HandlerFunc(app.handleMeAPI)))
	mux.Handle("/api/skills", app.auth.RequireAuth(http.HandlerFunc(app.handleSkillsAPI)))
	mux.Handle("/api/upload", app.auth.RequireAuth(http.HandlerFunc(app.handleUploadAPI)))
	mux.Handle("/api/purchase", app.auth.RequireAuth(http.HandlerFunc(app.handlePurchaseAPI)))
	mux.Handle("/api/my-skills", app.auth.RequireAuth(http.HandlerFunc(app.handleMySkillsAPI)))

	addr := ":" + cfg.Port
	log.Printf("server listening on %s", addr)
	if err := http.ListenAndServe(addr, loggingMiddleware(mux)); err != nil {
		log.Fatalf("server failed: %v", err)
	}
}

func runMigrations(db *sql.DB) error {
	schema, err := os.ReadFile("db/schema.sql")
	if err != nil {
		return fmt.Errorf("read schema: %w", err)
	}
	_, err = db.Exec(string(schema))
	if err != nil {
		return fmt.Errorf("exec schema: %w", err)
	}
	return nil
}

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		lrw := &loggingResponseWriter{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(lrw, r)

		ip := clientIP(r)
		log.Printf("method=%s path=%s status=%d bytes=%d duration=%s ip=%s ua=%q",
			r.Method,
			r.URL.Path,
			lrw.status,
			lrw.bytes,
			time.Since(start).Round(time.Millisecond),
			ip,
			r.UserAgent(),
		)
	})
}

// loggingResponseWriter captures status and response size for access logging.
type loggingResponseWriter struct {
	http.ResponseWriter
	status int
	bytes  int
}

func (w *loggingResponseWriter) WriteHeader(code int) {
	w.status = code
	w.ResponseWriter.WriteHeader(code)
}

func (w *loggingResponseWriter) Write(p []byte) (int, error) {
	n, err := w.ResponseWriter.Write(p)
	w.bytes += n
	return n, err
}

func clientIP(r *http.Request) string {
	if xff := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); xff != "" {
		parts := strings.Split(xff, ",")
		if len(parts) > 0 {
			return strings.TrimSpace(parts[0])
		}
	}
	if xrip := strings.TrimSpace(r.Header.Get("X-Real-Ip")); xrip != "" {
		return xrip
	}
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err == nil && host != "" {
		return host
	}
	return r.RemoteAddr
}

func (a *App) handleHome(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	a.render(w, "home.html", map[string]any{
		"Title":   "LLM Skill Marketplace",
		"DevMode": a.devMode,
	})
}

func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
	user := MustUserFromContext(r.Context())
	a.render(w, "dashboard.html", map[string]any{
		"Title":     "Dashboard",
		"UserEmail": user.Email,
	})
}

func (a *App) handleSkills(w http.ResponseWriter, r *http.Request) {
	a.render(w, "skills.html", map[string]any{
		"Title": "Browse Skills",
	})
}

func (a *App) handleMySkills(w http.ResponseWriter, r *http.Request) {
	a.render(w, "my_skills.html", map[string]any{
		"Title": "My Skills",
	})
}

func (a *App) handlePurchase(w http.ResponseWriter, r *http.Request) {
	a.render(w, "purchase.html", map[string]any{
		"Title": "Purchase Complete",
	})
}

func (a *App) handleMeAPI(w http.ResponseWriter, r *http.Request) {
	user := MustUserFromContext(r.Context())
	writeJSON(w, http.StatusOK, map[string]any{
		"sub":   user.Sub,
		"email": user.Email,
		"name":  user.Name,
	})
}

func (a *App) handleSkillsAPI(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		rows, err := a.db.QueryContext(r.Context(), `
			SELECT s.id, s.owner_sub, u.email, s.title, s.description, s.price_cents, s.file_path, s.created_at
			FROM skills s
			JOIN users u ON u.sub = s.owner_sub
			ORDER BY s.created_at DESC
		`)
		if err != nil {
			http.Error(w, "failed to load skills", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var skills []map[string]any
		for rows.Next() {
			var sk Skill
			if err := rows.Scan(&sk.ID, &sk.OwnerSub, &sk.OwnerEmail, &sk.Title, &sk.Description, &sk.PriceCents, &sk.FilePath, &sk.CreatedAt); err != nil {
				http.Error(w, "failed to read skills", http.StatusInternalServerError)
				return
			}
			skills = append(skills, map[string]any{
				"id":          sk.ID,
				"title":       sk.Title,
				"description": sk.Description,
				"price_cents": sk.PriceCents,
				"owner_email": sk.OwnerEmail,
				"created_at":  sk.CreatedAt,
			})
		}
		writeJSON(w, http.StatusOK, map[string]any{"skills": skills})
	default:
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
	}
}

func (a *App) handleUploadAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	user := MustUserFromContext(r.Context())

	if err := r.ParseMultipartForm(20 << 20); err != nil {
		http.Error(w, "invalid form data", http.StatusBadRequest)
		return
	}

	title := strings.TrimSpace(r.FormValue("title"))
	description := strings.TrimSpace(r.FormValue("description"))
	priceStr := strings.TrimSpace(r.FormValue("price_cents"))
	if title == "" || description == "" || priceStr == "" {
		http.Error(w, "title, description, and price_cents are required", http.StatusBadRequest)
		return
	}

	priceCents, err := strconv.Atoi(priceStr)
	if err != nil || priceCents < 0 {
		http.Error(w, "invalid price_cents", http.StatusBadRequest)
		return
	}

	file, header, err := r.FormFile("skill_file")
	if err != nil {
		http.Error(w, "skill_file is required", http.StatusBadRequest)
		return
	}
	defer file.Close()

	safeName := sanitizeFilename(header.Filename)
	if safeName == "" {
		http.Error(w, "invalid filename", http.StatusBadRequest)
		return
	}
	storedName := fmt.Sprintf("%d_%s_%s", time.Now().UnixNano(), user.Sub, safeName)
	fullPath := filepath.Join(a.storageDir, storedName)

	dst, err := os.Create(fullPath)
	if err != nil {
		http.Error(w, "failed to store file", http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	if _, err := io.Copy(dst, file); err != nil {
		http.Error(w, "failed to write file", http.StatusInternalServerError)
		return
	}

	var skillID int64
	err = a.db.QueryRowContext(r.Context(), `
		INSERT INTO skills (owner_sub, title, description, price_cents, file_path)
		VALUES ($1, $2, $3, $4, $5)
		RETURNING id
	`, user.Sub, title, description, priceCents, storedName).Scan(&skillID)
	if err != nil {
		http.Error(w, "failed to save skill", http.StatusInternalServerError)
		return
	}

	writeJSON(w, http.StatusCreated, map[string]any{
		"id": skillID,
	})
}

func (a *App) handlePurchaseAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	user := MustUserFromContext(r.Context())
	skillIDStr := strings.TrimSpace(r.FormValue("skill_id"))
	skillID, err := strconv.ParseInt(skillIDStr, 10, 64)
	if err != nil || skillID <= 0 {
		http.Error(w, "invalid skill_id", http.StatusBadRequest)
		return
	}

	ctx := r.Context()
	tx, err := a.db.BeginTx(ctx, nil)
	if err != nil {
		http.Error(w, "failed to start purchase", http.StatusInternalServerError)
		return
	}
	defer tx.Rollback()

	var ownerSub string
	err = tx.QueryRowContext(ctx, `SELECT owner_sub FROM skills WHERE id = $1`, skillID).Scan(&ownerSub)
	if errors.Is(err, sql.ErrNoRows) {
		http.Error(w, "skill not found", http.StatusNotFound)
		return
	}
	if err != nil {
		http.Error(w, "failed to load skill", http.StatusInternalServerError)
		return
	}
	if ownerSub == user.Sub {
		http.Error(w, "cannot buy your own skill", http.StatusBadRequest)
		return
	}

	_, err = tx.ExecContext(ctx, `
		INSERT INTO purchases (buyer_sub, skill_id)
		VALUES ($1, $2)
		ON CONFLICT (buyer_sub, skill_id) DO NOTHING
	`, user.Sub, skillID)
	if err != nil {
		http.Error(w, "failed to purchase skill", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(); err != nil {
		http.Error(w, "failed to complete purchase", http.StatusInternalServerError)
		return
	}

	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

func (a *App) handleMySkillsAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	user := MustUserFromContext(r.Context())

	rows, err := a.db.QueryContext(r.Context(), `
		SELECT s.id, s.title, s.description, u.email, p.purchased_at
		FROM purchases p
		JOIN skills s ON s.id = p.skill_id
		JOIN users u ON u.sub = s.owner_sub
		WHERE p.buyer_sub = $1
		ORDER BY p.purchased_at DESC
	`, user.Sub)
	if err != nil {
		http.Error(w, "failed to load purchased skills", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var skills []map[string]any
	for rows.Next() {
		var s UserSkill
		if err := rows.Scan(&s.SkillID, &s.Title, &s.Description, &s.OwnerEmail, &s.PurchasedAt); err != nil {
			http.Error(w, "failed to read purchased skills", http.StatusInternalServerError)
			return
		}
		skills = append(skills, map[string]any{
			"skill_id":      s.SkillID,
			"title":         s.Title,
			"description":   s.Description,
			"owner_email":   s.OwnerEmail,
			"purchased_at":  s.PurchasedAt,
			"copy_endpoint": fmt.Sprintf("/uploads/%d", s.SkillID),
		})
	}

	writeJSON(w, http.StatusOK, map[string]any{"skills": skills})
}

func (a *App) handleDownload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	user := MustUserFromContext(r.Context())
	idPart := strings.TrimPrefix(r.URL.Path, "/uploads/")
	skillID, err := strconv.ParseInt(idPart, 10, 64)
	if err != nil || skillID <= 0 {
		http.NotFound(w, r)
		return
	}

	var ownerSub, path string
	err = a.db.QueryRowContext(r.Context(), `SELECT owner_sub, file_path FROM skills WHERE id = $1`, skillID).Scan(&ownerSub, &path)
	if errors.Is(err, sql.ErrNoRows) {
		http.NotFound(w, r)
		return
	}
	if err != nil {
		http.Error(w, "failed to read file", http.StatusInternalServerError)
		return
	}

	allowed := ownerSub == user.Sub
	if !allowed {
		var exists int
		err := a.db.QueryRowContext(r.Context(), `
			SELECT 1 FROM purchases WHERE buyer_sub = $1 AND skill_id = $2
		`, user.Sub, skillID).Scan(&exists)
		if err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				http.Error(w, "purchase required", http.StatusForbidden)
				return
			}
			http.Error(w, "failed authorization check", http.StatusInternalServerError)
			return
		}
	}

	full := filepath.Join(a.storageDir, path)
	w.Header().Set("Content-Disposition", "attachment; filename=\"skill_"+strconv.FormatInt(skillID, 10)+"\"")
	http.ServeFile(w, r, full)
}

func (a *App) render(w http.ResponseWriter, name string, data map[string]any) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := a.templates.ExecuteTemplate(w, name, data); err != nil {
		http.Error(w, "template rendering failed", http.StatusInternalServerError)
	}
}

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

func sanitizeFilename(name string) string {
	name = filepath.Base(name)
	name = strings.ReplaceAll(name, " ", "_")
	name = strings.Map(func(r rune) rune {
		switch {
		case r >= 'a' && r <= 'z':
			return r
		case r >= 'A' && r <= 'Z':
			return r
		case r >= '0' && r <= '9':
			return r
		case r == '.', r == '-', r == '_':
			return r
		default:
			return -1
		}
	}, name)
	return name
}
