package api

import (
	"context"
	"io"
	"database/sql"
	"errors"
	"encoding/json"
	"log"
	"net/http"
	"net/url"
	"time"

	"wikiapiserver/db"
)

const defaultTimeout = 5 * time.Second

// Handler holds the DB dependency for all HTTP handlers.
type Handler struct {
	db *db.DB
}

// NewHandler creates a Handler backed by the given DB.
func NewHandler(database *db.DB) *Handler {
	return &Handler{db: database}
}

// --- request/response types ---

type errResp struct {
	Error string `json:"error"`
}

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

type loginReq struct {
	Username string `json:"username"`
	Password string `json:"password"`
}
// tokenResp is returned by GET /token.
type tokenResp struct {
	AccessToken string `json:"access_token"`
	ValidUntil  string `json:"valid_until"`
}
// --- helper writers ---

func writeJSON(w http.ResponseWriter, code int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(v) //nolint:errcheck
}

func badRequest(w http.ResponseWriter, msg string) {
	writeJSON(w, http.StatusBadRequest, errResp{Error: msg})
}

func unauthorized(w http.ResponseWriter) {
	writeJSON(w, http.StatusUnauthorized, errResp{Error: "unauthorized"})
}

func serverError(w http.ResponseWriter, msg string) {
	writeJSON(w, http.StatusInternalServerError, errResp{Error: msg})
}

// --- Register: POST /register ---

func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), defaultTimeout)
	defer cancel()

	var req registerReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		badRequest(w, "invalid JSON")
		return
	}

	if req.Username == "" || req.Password == "" {
		badRequest(w, "username and password are required")
		return
	}

	acct, err := h.db.Register(ctx, req.Username, req.Password)
	if err != nil {
		if err.Error() == "username already exists" {
			badRequest(w, "username already exists")
			return
		}
		log.Printf("register error: %v", err)
		serverError(w, "could not create account")
		return
	}

	writeJSON(w, http.StatusCreated, acct)
}

// --- Login: POST /login ---

func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), defaultTimeout)
	defer cancel()

	var req loginReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		badRequest(w, "invalid JSON")
		return
	}

	if req.Username == "" || req.Password == "" {
		badRequest(w, "username and password are required")
		return
	}

	acct, err := h.db.Authenticate(ctx, req.Username, req.Password)
	if err != nil {
		if err.Error() == "invalid credentials" {
			unauthorized(w)
			return
		}
		serverError(w, "authentication failed")
		return
	}

	writeJSON(w, http.StatusOK, acct)
}

// --- Refresh: POST /refresh ---
// Accepts username and refresh_token. The refresh_token is used to
// verify identity; RefreshTokens handles the age-based logic.
type refreshReq struct {
	Username     string `json:"username"`
	RefreshToken string `json:"refresh_token"`
}

func (h *Handler) Refresh(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), defaultTimeout)
	defer cancel()

	var req refreshReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		badRequest(w, "invalid JSON")
		return
	}

	if req.Username == "" || req.RefreshToken == "" {
		badRequest(w, "username and refresh_token are required")
		return
	}
	acct, err := h.db.RefreshTokens(ctx, req.Username, req.RefreshToken)
	if err != nil {
		if err.Error() == "account not found" || err.Error() == "invalid refresh token" {
			unauthorized(w)
			return
		}
		log.Printf("refresh error: %v", err)
		serverError(w, "could not refresh token")
		return
	}

	writeJSON(w, http.StatusOK, acct)
}

// --- Health: GET /health ---

func (h *Handler) Health(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), defaultTimeout)
	defer cancel()

	if err := h.db.Ping(ctx); err != nil {
		writeJSON(w, http.StatusServiceUnavailable, errResp{Error: "database unavailable"})
		return
	}

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

// --- Get Token: GET /token?username=... ---

func (h *Handler) GetToken(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), defaultTimeout)
	defer cancel()

	username := r.URL.Query().Get("username")
	if username == "" {
		badRequest(w, "username query parameter is required")
		return
	}

	acct, err := h.db.GetAccount(ctx, username)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			unauthorized(w)
			return
		}
		serverError(w, "could not retrieve token")
		return
	}

	writeJSON(w, http.StatusOK, tokenResp{
		AccessToken: acct.AccessToken,
		ValidUntil:  acct.AccessTokenExpiry.Add(24 * time.Hour).Format(time.RFC3339),
	})
}
// --- Get Article: GET /article?username=...&name=... ---

func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
	defer cancel()

	username := r.URL.Query().Get("username")
	article := r.URL.Query().Get("name")
	if username == "" || article == "" {
		badRequest(w, "username and name query parameters are required")
		return
	}

	acct, err := h.db.GetAccount(ctx, username)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			unauthorized(w)
			return
		}
		serverError(w, "could not retrieve token")
		return
	}

	baseURL := "https://api.enterprise.wikimedia.com/v2/structured-contents/" + url.QueryEscape(article)
	queryURL := baseURL + "?limit=1"

	req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil)
	if err != nil {
		serverError(w, "could not build request")
		return
	}
	req.Header.Set("Authorization", "Bearer "+acct.AccessToken)

	start := time.Now()
	resp, err := http.DefaultClient.Do(req)
	elapsed := int(time.Since(start).Milliseconds())

	if err != nil {
		log.Printf("article request failed: user=%s article=%s error=%v (%dms)", username, article, err, elapsed)
		h.db.LogApiCall(ctx, &db.ApiLogEntry{
			Username:    username,
			ArticleName: article,
			StatusCode:  0,
			ResponseMs:  elapsed,
			Error:       err.Error(),
			RequestURL:  queryURL,
		})
		serverError(w, "wikimedia api error")
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		b, _ := io.ReadAll(resp.Body)
		resp.Body.Close()

		log.Printf("article request failed: user=%s article=%s status=%d body=%s (%dms)",
			username, article, resp.StatusCode, string(b), elapsed)
		h.db.LogApiCall(ctx, &db.ApiLogEntry{
			Username:    username,
			ArticleName: article,
			StatusCode:  resp.StatusCode,
			ResponseMs:  elapsed,
			Error:       string(b),
			RequestURL:  queryURL,
		})

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(resp.StatusCode)
		w.Write(b) //nolint:errcheck
		if f, ok := w.(http.Flusher); ok {
			f.Flush()
		}
		return
	}

	w.Header().Set("Content-Type", "application/json")
	io.Copy(w, resp.Body) //nolint:errcheck
}
