// Package bookstore provides an HTTP-based book catalogue service.
//
// It demonstrates struct methods, interfaces, error handling, and a
// simple HTTP handler following standard Go conventions.
package bookstore

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"
)

// Common sentinel errors returned by the catalogue.
var (
	ErrNotFound       = errors.New("book not found")
	ErrDuplicateISBN  = errors.New("duplicate ISBN")
	ErrInvalidBook    = errors.New("invalid book data")
)

// Book represents a single volume in the catalogue.
type Book struct {
	ISBN      string    `json:"isbn"`
	Title     string    `json:"title"`
	Author    string    `json:"author"`
	Year      int       `json:"year"`
	Pages     int       `json:"pages"`
	Available bool      `json:"available"`
	AddedAt   time.Time `json:"added_at"`
}

// Validate ensures all required fields are present.
func (b Book) Validate() error {
	if strings.TrimSpace(b.ISBN) == "" {
		return fmt.Errorf("%w: ISBN is required", ErrInvalidBook)
	}
	if strings.TrimSpace(b.Title) == "" {
		return fmt.Errorf("%w: title is required", ErrInvalidBook)
	}
	if b.Year < 1000 || b.Year > time.Now().Year()+1 {
		return fmt.Errorf("%w: year %d is out of range", ErrInvalidBook, b.Year)
	}
	return nil
}

// Repository defines the interface that any book storage must satisfy.
type Repository interface {
	GetByISBN(isbn string) (Book, error)
	List() ([]Book, error)
	Add(book Book) error
	Remove(isbn string) error
	Search(query string) ([]Book, error)
}

// InMemoryRepo is a thread-safe, in-memory implementation of Repository.
type InMemoryRepo struct {
	mu    sync.RWMutex
	books map[string]Book
}

// NewInMemoryRepo returns an initialised in-memory repository.
func NewInMemoryRepo() *InMemoryRepo {
	return &InMemoryRepo{books: make(map[string]Book)}
}

func (r *InMemoryRepo) GetByISBN(isbn string) (Book, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	book, ok := r.books[isbn]
	if !ok {
		return Book{}, fmt.Errorf("%w: %s", ErrNotFound, isbn)
	}
	return book, nil
}

func (r *InMemoryRepo) List() ([]Book, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	result := make([]Book, 0, len(r.books))
	for _, b := range r.books {
		result = append(result, b)
	}
	return result, nil
}

func (r *InMemoryRepo) Add(book Book) error {
	if err := book.Validate(); err != nil {
		return err
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, exists := r.books[book.ISBN]; exists {
		return fmt.Errorf("%w: %s", ErrDuplicateISBN, book.ISBN)
	}
	book.AddedAt = time.Now()
	book.Available = true
	r.books[book.ISBN] = book
	return nil
}

func (r *InMemoryRepo) Remove(isbn string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, ok := r.books[isbn]; !ok {
		return fmt.Errorf("%w: %s", ErrNotFound, isbn)
	}
	delete(r.books, isbn)
	return nil
}

func (r *InMemoryRepo) Search(query string) ([]Book, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	q := strings.ToLower(query)
	var matched []Book
	for _, b := range r.books {
		if strings.Contains(strings.ToLower(b.Title), q) ||
			strings.Contains(strings.ToLower(b.Author), q) {
			matched = append(matched, b)
		}
	}
	return matched, nil
}

// Handler exposes the catalogue over HTTP.
type Handler struct {
	repo Repository
}

// NewHandler creates a Handler backed by the given Repository.
func NewHandler(repo Repository) *Handler {
	return &Handler{repo: repo}
}

// ServeHTTP routes requests to the appropriate method handler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	switch r.Method {
	case http.MethodGet:
		h.handleList(w, r)
	case http.MethodPost:
		h.handleAdd(w, r)
	default:
		http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
	}
}

func (h *Handler) handleList(w http.ResponseWriter, _ *http.Request) {
	books, err := h.repo.List()
	if err != nil {
		http.Error(w, fmt.Sprintf(`{"error":"%s"}`, err), http.StatusInternalServerError)
		return
	}
	json.NewEncoder(w).Encode(books)
}

func (h *Handler) handleAdd(w http.ResponseWriter, r *http.Request) {
	var book Book
	if err := json.NewDecoder(r.Body).Decode(&book); err != nil {
		http.Error(w, `{"error":"invalid JSON body"}`, http.StatusBadRequest)
		return
	}
	if err := h.repo.Add(book); err != nil {
		status := http.StatusInternalServerError
		if errors.Is(err, ErrInvalidBook) {
			status = http.StatusUnprocessableEntity
		} else if errors.Is(err, ErrDuplicateISBN) {
			status = http.StatusConflict
		}
		http.Error(w, fmt.Sprintf(`{"error":"%s"}`, err), status)
		return
	}
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(book)
}