package api

import (
	"encoding/json"
	"errors"
	"net/http"
	"strconv"

	"github.com/go-chi/chi/v5"
	chimw "github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/cors"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"forum-backend/internal/auth"
	"forum-backend/internal/config"
)

// App holds shared dependencies injected into every handler.
type App struct {
	db     *pgxpool.Pool
	config config.Config
}

func NewApp(pool *pgxpool.Pool, cfg config.Config) *App {
	return &App{db: pool, config: cfg}
}

// BuildRouter wires all routes and middleware onto a chi router.
func BuildRouter(app *App) http.Handler {
	r := chi.NewRouter()

	r.Use(chimw.Logger)
	r.Use(cors.Handler(cors.Options{
		// Restrict to localhost in line with OWASP CORS guidance.
		// AllowCredentials is required so the browser sends the __Secure-Fgp cookie
		// alongside the Bearer token; this is the OWASP token-sidejacking defense.
		AllowedOrigins:   []string{"http://localhost", "http://localhost:80"},
		AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
		AllowedHeaders:   []string{"Authorization", "Content-Type"},
		ExposedHeaders:   []string{"X-Served-By"},
		AllowCredentials: true,
	}))
	r.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("X-Served-By", app.config.InstanceID)
			next.ServeHTTP(w, r)
		})
	})

	// Public routes
	r.Get("/api/health", app.health)
	r.Post("/api/auth/register", app.register)
	r.Post("/api/auth/login", app.login)
	r.Post("/api/auth/refresh", app.refresh)
	r.Post("/api/auth/logout", app.logout)
	r.Get("/api/users/{userID}", app.getUser)
	r.Get("/api/categories", app.listCategories)
	r.Get("/api/categories/{categoryID}", app.getCategory)
	r.Get("/api/categories/{categoryID}/threads", app.listThreads)
	r.Get("/api/threads/{threadID}/posts", app.listPosts)

	// Protected routes
	r.Group(func(r chi.Router) {
		r.Use(auth.RequireAuth(app.config.JWTSecret, app.config.JWTIssuer))
		r.Get("/api/users/me", app.me)
		r.Post("/api/categories/{categoryID}/threads", app.createThread)
		r.Post("/api/threads/{threadID}/posts", app.createPost)
		r.Put("/api/posts/{postID}", app.updatePost)
		r.Delete("/api/posts/{postID}", app.deletePost)
	})

	return r
}

// ── error handling ────────────────────────────────────────────────────────────

type appError struct {
	code    int
	message string
}

func (e appError) Error() string { return e.message }

var (
	errNotFound     = appError{http.StatusNotFound, "not found"}
	errUnauthorized = appError{http.StatusUnauthorized, "unauthorized"}
	errForbidden    = appError{http.StatusForbidden, "forbidden"}
)

func conflict(msg string) appError      { return appError{http.StatusConflict, msg} }
func unprocessable(msg string) appError { return appError{http.StatusUnprocessableEntity, msg} }
func internal(msg string) appError      { return appError{http.StatusInternalServerError, msg} }

func pgxToAppError(err error) error {
	if errors.Is(err, pgx.ErrNoRows) {
		return errNotFound
	}
	var pgErr interface{ SQLState() string }
	if errors.As(err, &pgErr) && pgErr.SQLState() == "23505" {
		return conflict("resource already exists")
	}
	return internal(err.Error())
}

func writeError(w http.ResponseWriter, err error) {
	code := http.StatusInternalServerError
	msg := err.Error()
	var ae appError
	if errors.As(err, &ae) {
		code = ae.code
		msg = ae.message
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
}

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

// ── shared helpers ────────────────────────────────────────────────────────────

func parsePagination(r *http.Request) (page, perPage int64) {
	page, _ = strconv.ParseInt(r.URL.Query().Get("page"), 10, 64)
	if page < 1 {
		page = 1
	}
	perPage, _ = strconv.ParseInt(r.URL.Query().Get("per_page"), 10, 64)
	if perPage < 1 {
		perPage = 20
	}
	return
}
