package sklep.rest;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import sklep.model.Product;
import sklep.photo.PhotoUtil;
import sklep.repository.ProductRepository;

import java.math.BigDecimal;
import java.util.List;

// @RestController odpowiada adnotacji @Controller z dodatkową adnotacją @ResponseBody
// Inaczej mówiąc, metody nie zwracają nazw szablonów, lecz dane, które mają być odesłane do klienta.
@RestController
@RequestMapping("/rest/products")
public class ProductEndpoint {
    private ProductRepository productRepository;
    private PhotoUtil photoUtil;

    // Tutaj stosujemy wstrzykiwanie przez konstruktor.
    public ProductEndpoint(ProductRepository productRepository, PhotoUtil photoUtil) {
        this.productRepository = productRepository;
        this.photoUtil = photoUtil;
    }

    @GetMapping(produces="application/json")
    public List<Product> getProducts() {
        return productRepository.findAll();
    }

    @GetMapping(path="/{id}", produces="application/json")
    public Product getProduct(@PathVariable int id) {
        return productRepository.findById(id).orElseThrow(
                () -> new ResponseStatusException(HttpStatus.NOT_FOUND,
                        "Brak produktu o numerze " + id));
    }

    // Ta metoda pokazana tylko po to, aby wytłumaczyć, że struktura adresów powinna odpowiadać logicznej strukturze danych.
    // W prawdziwej aplikacji raczej nie dochodzi się do poziomu pojedynczych pól. Teoretycznie można.
    @GetMapping(path="/{id}/price", produces="application/json")
    public BigDecimal getPrice(@PathVariable Integer id) {
        return getProduct(id).getPrice();
    }

    /*
     * Operacja PUT służy do zapisania danych POD PODANYM ADRESEM.
     * Na przykład PUT products/2/price z wartością 100 powinno ustawić w produkcie nr 2 cenę 100.
     * Jeśli PUT zadziała, to następnie GET wysłany pod ten sam adres powinien odczytać te same dane,
     * które PUT zapisał (być może w innym formacie - to inny temat)
     *
     * PUT najczęściej jest używany do aktualizacji istniejących danych
     * (pojedynczych wartości albo całych rekordów), ale może być też użyty do
     * zapisania nowych danych. To, co najważniejsze, to fakt, że PUT zapisuje dane
     * pod konkretnym adresem, do którego jest wysyłany.
     *
     * Aby odczytać dane, które przysłał klient, metoda ma jeden parametr oznaczony @RequestBody.
     * To do tego parametru Spring przekaże dane odczytane z "body" zapytania.
     */
    @PutMapping("/{id}/price")
    public void setPrice(@PathVariable Integer id, @RequestBody BigDecimal newPrice) {
        Product product = productRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        product.setPrice(newPrice);
        productRepository.save(product);
    }

    /*
     * PUT może również służyć do zapisania całego rekordu, ale zwn, że musi być
     * skierowany pod ten adres, pod którym rekord zostanie faktycznie zapisany, w
     * praktyce PUT jest uzywany do aktualizacji rekordów (UPDATE).
     *
     * Aby w aplikacji Springowej, w której jest włączone security, działały zapytania POST i PUT,
     * trzeba wyłączyć zabezpieczenie "CSRF":
     * .and().csrf().disable()
     */
    @PutMapping("/{id}")
    public void update(@PathVariable("id") Integer productId, @RequestBody Product product) {
        // Aby mieć pewność, że zapisujemu produkt o typ ID, wpisuję productId z URL-a.
        // Ewentualnie możnaby jeszcze sprawdzić czy rekord istnieje, czy ID się zgadza
        // i jeśli coś jest nie tak, to wyrzucić wyjątek.
        product.setId(productId);
        productRepository.save(product);
    }

    /*
     * POST jest najbardziej ogólną metodą HTTP; oznacza, że klient
     * "wysyła jakieś dane na serwer", a serwer odsyła jakąś odpowiedź.
     * W zasadzie POST może służyć do wszystkiego.
     *
     * W praktyce POST bardzo często służy do dodawania nowych rekordów, ponieważ
     * gdy tworzymy nowy rekord, to nie znamy z góry jego ID i nie wiemy pod jakim
     * URL-em zostanie zapisany (nie da się więc użyć PUT). Stąd wzięła REST-owa
     * konwencja, że aby dodać nowy rekord do katalogu, wysyła się POST z danymi
     * tego rekordu pod ogólny adres całego katalogu.
     */
    @PostMapping
    public Product insert(@RequestBody Product product) {
        // Aby mieć pewność, że zapytanie tworzy nowy rekord, ustawiam productId na null.
        product.setId(null);
        productRepository.save(product);
        /* Operacja save (a wewnętrznie persist z Hibernate) spowoduje ustawienie nowego ID w obiekcie.
         * Warto taką informację przekazać klientowi. Można:
         * 1) odesłać uzupełniony rekord (i tak zrobimy tutaj),
         * 2) odesłać "małego JSON-a" z informacją o tym ID
         *    (i innymi informacjami, które serwer chce przekazać klientowi)
         * 3) tylko nagłówek Location z URL-em nowego rekordu (to zobaczymy w wersji JAX-RS).
         */
        return product;
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable("id") Integer productId) {
        try {
            productRepository.deleteById(productId);
        } catch (EmptyResultDataAccessException e) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Brak produktu nr " + productId);
        }
    }

    /* Większość komunikacji w usługach REST odbywa się w formacie JSON,
     * ale czasami używany jest też format XML,
     * a dla niektórych danych stosujemy bezpośrednio jakiś format specjalny, np. PNG, JPG dla obrazów, PDF dla wydruków itp.
     */
    @GetMapping(path="/{id}/photo", produces="image/jpeg")
    public byte[] getPhoto(@PathVariable("id") int productId) {
        return photoUtil.readBytes(productId);
    }

    @PutMapping(path="/{id}/photo", consumes="image/jpeg")
    public void uploadPhoto(@PathVariable("id") int productId, @RequestBody byte[] bytes) {
        photoUtil.writeBytes(productId, bytes);
    }

}
