/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache license, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the license for the specific language governing permissions and
 * limitations under the license.
 */
package org.apache.logging.log4j.audit.service.controller;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.annotation.PostConstruct;

import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.audit.service.catalog.AuditManager;
import org.apache.logging.log4j.catalog.api.Attribute;
import org.apache.logging.log4j.catalog.api.Category;
import org.apache.logging.log4j.catalog.api.Event;
import org.apache.logging.log4j.catalog.api.Product;
import org.apache.logging.log4j.catalog.api.Versions;
import org.apache.logging.log4j.catalog.jpa.converter.AttributeConverter;
import org.apache.logging.log4j.catalog.jpa.converter.AttributeModelConverter;
import org.apache.logging.log4j.catalog.jpa.converter.CategoryConverter;
import org.apache.logging.log4j.catalog.jpa.converter.CategoryModelConverter;
import org.apache.logging.log4j.catalog.jpa.converter.EventConverter;
import org.apache.logging.log4j.catalog.jpa.converter.EventModelConverter;
import org.apache.logging.log4j.catalog.jpa.converter.ProductConverter;
import org.apache.logging.log4j.catalog.jpa.converter.ProductModelConverter;
import org.apache.logging.log4j.catalog.jpa.model.AttributeModel;
import org.apache.logging.log4j.catalog.jpa.model.CategoryModel;
import org.apache.logging.log4j.catalog.jpa.model.EventModel;
import org.apache.logging.log4j.catalog.jpa.model.ProductModel;
import org.apache.logging.log4j.catalog.jpa.service.AttributeService;
import org.apache.logging.log4j.catalog.jpa.service.CategoryService;
import org.apache.logging.log4j.catalog.jpa.service.EventService;
import org.apache.logging.log4j.catalog.jpa.service.ProductService;
import org.modelmapper.ModelMapper;
import org.modelmapper.TypeToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import static org.apache.logging.log4j.catalog.api.constant.Constants.DEFAULT_CATALOG;

@RestController
@RequestMapping(value = "/catalog")
public class CatalogController {

    private static final Logger LOGGER = LogManager.getLogger(CatalogController.class);

    private final ModelMapper attributeModelMapper = new ModelMapper();
    private final ModelMapper eventModelMapper = new ModelMapper();
    private final ModelMapper productModelMapper = new ModelMapper();
    private final ModelMapper categoryModelMapper = new ModelMapper();

    @Autowired
    private AttributeService attributeService;

    @Autowired
    private AttributeModelConverter attributeModelConverter;

    @Autowired
    private AttributeConverter attributeConverter;

    @Autowired
    private EventService eventService;

    @Autowired
    private EventModelConverter eventModelConverter;

    @Autowired
    private EventConverter eventConverter;

    @Autowired
    private ProductService productService;

    @Autowired
    private ProductModelConverter productModelConverter;

    @Autowired
    private ProductConverter productConverter;

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private AuditManager auditManager;

    @Autowired
    private CategoryModelConverter categoryModelConverter;

    @Autowired
    private CategoryConverter categoryConverter;

    @PostConstruct
    public void init() {
        attributeModelMapper.addConverter(attributeModelConverter);
        eventModelMapper.addConverter(eventModelConverter);
        productModelMapper.addConverter(productModelConverter);
        categoryModelMapper.addConverter(categoryModelConverter);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "List catalog Attributes", notes = "List catalog attributes for a catalog id", tags = {"Catalog"})
    @GetMapping(value = "{catalogId}/attributes")
    public ResponseEntity<List<Attribute>> getAttributes(@ApiParam(value = "catalog id", required = true) @PathVariable String catalogId,
                                                         @RequestParam(value = "startIndex", required = false) Integer startIndex,
                                                         @RequestParam(value = "pageSize", required = false) Integer pageSize,
                                                         @RequestParam(value = "sortCol", required= false) String sortColumn,
                                                         @RequestParam(value = "sortDir", required = false) String sortDirection) {
        Type listType = new TypeToken<List<Attribute>>() {
        }.getType();
        List<Attribute> attributes;
        if (startIndex == null || pageSize == null) {
            attributes = attributeModelMapper.map(attributeService.getAttributes(catalogId), listType);
        } else {
            sortDirection = validateSortDirection(sortDirection);
            if (sortColumn == null || sortColumn.length() == 0) {
                sortColumn = "name";
            }
            int startPage = 0;
            if (startIndex > 0) {
                startPage = startIndex / pageSize;
            }
            attributes = attributeModelMapper.map(attributeService.getAttributes(startPage, pageSize, sortColumn,
                    sortDirection), listType);
        }
        if (attributes == null) {
            attributes = new ArrayList<>();
        }
        return new ResponseEntity<>(attributes, HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Create a catalog Attribute", notes = "Returns a catalog attribute", tags = {"Catalog"})
    @GetMapping(value = "{catalogId}/attribute/{name}")
    public ResponseEntity<Attribute> getAttribute(@ApiParam(value = "catalog id", required = true) @PathVariable String catalogId,
                                                         @ApiParam(value = "attribute name", required = true) @PathVariable String name) {
        Optional<AttributeModel> optional = attributeService.getAttribute(catalogId, name);
        if (!optional.isPresent()) {
            LOGGER.warn("Unable to locate attribute {} in catalog {}", name, catalogId);
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        Attribute attribute = attributeModelConverter.convert(optional.get());
        return new ResponseEntity<>(attribute, HttpStatus.OK);
    }



    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Create a catalog Attribute", notes = "Creates a catalog attribute", tags = {"Catalog"})
    @PostMapping(value = "/attribute", consumes=Versions.V1_0, produces=Versions.V1_0)
    public ResponseEntity<Attribute> createAttribute(@ApiParam(value = "attribute", required = true) @RequestBody Attribute attribute) {
        if (attribute.getCatalogId() == null) {
            throw new IllegalArgumentException("A catalog id is required.");
        }
        if (DEFAULT_CATALOG.equals(attribute.getCatalogId())) {
            throw new IllegalArgumentException("The default catalog cannot be modified at run time.");
        }
        AttributeModel model;
        synchronized(this) {
            Optional<AttributeModel> opt = attributeService.getAttribute(attribute.getCatalogId(), attribute.getName());
            if (opt != null && opt.isPresent()) {
                throw new IllegalStateException(
                    "An attribute named " + attribute.getName() + " in catalog " + attribute.getCatalogId() + " already exists");
            }
            model = attributeService.saveAttribute(attributeConverter.convert(attribute));
            auditManager.saveAttribute(attribute);
        }
        return new ResponseEntity<>(attributeModelConverter.convert(model), HttpStatus.CREATED);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Update a catalog Attribute", notes = "Updates a catalog attribute", tags = {"Catalog"})
    @PutMapping(value = "/attribute", consumes=Versions.V1_0, produces=Versions.V1_0)
    public ResponseEntity<Attribute> updateAttribute(@ApiParam(value = "attribute", required = true) @RequestBody Attribute attribute) {
        if (attribute.getId() == null) {
            throw new IllegalArgumentException("An Attribute must have an id to be updated.");
        }
        if (attribute.getCatalogId() == null) {
            throw new IllegalArgumentException("A catalog id is required in the Attribute.");
        }
        if (DEFAULT_CATALOG.equals(attribute.getCatalogId())) {
            throw new IllegalArgumentException("The default catalog cannot be modified at run time.");
        }
        AttributeModel model = attributeConverter.convert(attribute);
        model = attributeService.saveAttribute(model);
        auditManager.saveAttribute(attribute);
        return new ResponseEntity<>(attributeModelConverter.convert(model), HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Deletes a catalog Attribute", notes = "Deletes a catalog attribute", tags = {"Catalog"})
    @DeleteMapping(value = "/attribute/{id}")
    public ResponseEntity<?> deleteAttribute(@RequestParam("id") Long attributeId) {
        synchronized (this) {
            Optional<AttributeModel> opt = attributeService.getAttribute(attributeId);
            if (opt.isPresent()) {
                attributeService.deleteAttribute(attributeId);
            }
        }
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "List catalog Events", notes = "Lists catalog events for a catalog id", tags = {"Catalog"})
    @GetMapping(value = "{catalogId}/events")
    public ResponseEntity<List<Event>> getEvents(@ApiParam(value = "catalog id", required = true) @PathVariable String catalogId,
                                                         @RequestParam(value = "startIndex", required = false) Integer startIndex,
                                                         @RequestParam(value = "pageSize", required = false) Integer pageSize,
                                                         @RequestParam(value = "sortCol", required= false) String sortColumn,
                                                         @RequestParam(value = "sortDir", required = false) String sortDirection) {
        Type listType = new TypeToken<List<Event>>() {}.getType();
        List<Event> events;
        if (startIndex == null || pageSize == null) {
            events = eventModelMapper.map(eventService.getEvents(catalogId), listType);
        } else {
            sortDirection = validateSortDirection(sortDirection);
            if (sortColumn == null || sortColumn.length() == 0) {
                sortColumn = "name";
            }
            int startPage = 0;
            if (startIndex > 0) {
                startPage = startIndex / pageSize;
            }
            events = eventModelMapper.map(eventService.getEvents(startPage, pageSize, sortColumn,
                    sortDirection), listType);
        }
        if (events == null) {
            events = new ArrayList<>();
        }
        return new ResponseEntity<>(events, HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Create a catalog Event", notes = "Creates a catalog event", tags = {"Catalog"})
    @PostMapping(value = "/event", consumes=Versions.V1_0, produces=Versions.V1_0)
    public ResponseEntity<Event> createEvent(@ApiParam(value = "event", required = true) @RequestBody Event event) {
        if (event.getCatalogId() == null) {
            throw new IllegalArgumentException("A catalog id is required to create an event.");
        }
        if (DEFAULT_CATALOG.equals(event.getCatalogId())) {
            throw new IllegalArgumentException("The default catalog cannot be modified at run time.");
        }
        EventModel model;
        synchronized(this) {
            Optional<EventModel> opt = eventService.getEvent(event.getCatalogId(), event.getName());
            if (opt != null && opt.isPresent()) {
                throw new IllegalStateException(
                    "An event named " + event.getName() + " in catalog " + event.getCatalogId() + " already exists");
            }
            model = auditManager.saveEvent(event);
        }
        return new ResponseEntity<>(eventModelConverter.convert(model), HttpStatus.CREATED);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Update a catalog Event", notes = "Updates a catalog event", tags = {"Catalog"})
    @PutMapping(value = "/event", consumes=Versions.V1_0, produces=Versions.V1_0)
    public ResponseEntity<Event> updateEvent(@ApiParam(value = "event", required = true) @RequestBody Event event) {
        if (event.getCatalogId() == null) {
            throw new IllegalArgumentException("A catalog id is required to update an event.");
        }
        if (DEFAULT_CATALOG.equals(event.getCatalogId())) {
            throw new IllegalArgumentException("The default catalog cannot be modified at run time.");
        }
        EventModel model;
        synchronized(this) {
            model = eventConverter.convert(event);
            model = eventService.saveEvent(model);
        }
        return new ResponseEntity<>(eventModelConverter.convert(model), HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Deletes a catalog event", notes = "Deletes a catalog event", tags = {"Catalog"})
    @DeleteMapping(value = "/event/{id}")
    public ResponseEntity<?> deleteEvent(@RequestParam("id") Long eventId) {
        eventService.deleteEvent(eventId);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "List catalog Products", notes = "Lists catalog products for a catalog id", tags = {"Catalog"})
    @GetMapping(value = "{catalogId}/products")
    public ResponseEntity<List<Product>> getProducts(@ApiParam(value = "catalog id", required = true) @PathVariable String catalogId,
                                                 @RequestParam(value = "startIndex", required = false) Integer startIndex,
                                                 @RequestParam(value = "pageSize", required = false) Integer pageSize,
                                                 @RequestParam(value = "sortCol", required= false) String sortColumn,
                                                 @RequestParam(value = "sortDir", required = false) String sortDirection) {
        Type listType = new TypeToken<List<Product>>() {}.getType();
        List<Product> products;
        if (startIndex == null || pageSize == null) {
            products = productModelMapper.map(productService.getProducts(catalogId), listType);
        } else {
            sortDirection = validateSortDirection(sortDirection);
            if (sortColumn == null || sortColumn.length() == 0) {
                sortColumn = "name";
            }
            int startPage = 0;
            if (startIndex > 0) {
                startPage = startIndex / pageSize;
            }
            products = productModelMapper.map(productService.getProducts(startPage, pageSize, sortColumn,
                    sortDirection), listType);
        }
        if (products == null) {
            products = new ArrayList<>();
        }
        return new ResponseEntity<>(products, HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Create a catalog Product", notes = "Creates a catalog product", tags = {"Catalog"})
    @PostMapping(value = "/product", consumes=Versions.V1_0, produces=Versions.V1_0)
    public ResponseEntity<Product> createProduct(@ApiParam(value = "product", required = true) @RequestBody Product product) {
        if (product.getCatalogId() == null) {
            throw new IllegalArgumentException("A catalog id is required to create a product.");
        }
        if (DEFAULT_CATALOG.equals(product.getCatalogId())) {
            throw new IllegalArgumentException("The default catalog cannot be modified at run time.");
        }
        Optional<ProductModel> opt = productService.getProduct(product.getCatalogId(), product.getName());
        if (opt != null && opt.isPresent()) {
            throw new IllegalStateException("A product named "+ product.getName() + " in catalog " +
                    product.getCatalogId() + " already exists");
        }
        ProductModel model = productConverter.convert(product);
        model = productService.saveProduct(model);
        return new ResponseEntity<>(productModelConverter.convert(model), HttpStatus.CREATED);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Update a catalog Product", notes = "Updates a catalog event", tags = {"Catalog"})
    @PutMapping(value = "/product", consumes=Versions.V1_0, produces=Versions.V1_0)
    public ResponseEntity<Product> updateProduct(@ApiParam(value = "product", required = true) @RequestBody Product product) {
        if (product.getCatalogId() == null) {
            throw new IllegalArgumentException("A catalog id is required to update a product.");
        }
        if (DEFAULT_CATALOG.equals(product.getCatalogId())) {
            throw new IllegalArgumentException("The default catalog cannot be modified at run time.");
        }
        ProductModel model = productConverter.convert(product);
        model = productService.saveProduct(model);
        return new ResponseEntity<>(productModelConverter.convert(model), HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Deletes a catalog product", notes = "Deletes a catalog product", tags = {"Catalog"})
    @DeleteMapping(value = "/product/{id}")
    public ResponseEntity<?> deleteProduct(@RequestParam("id") Long productId) {
        productService.deleteProduct(productId);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "List catalog Categories", notes = "Lists catalog categories for a catalog id", tags = {"Catalog"})
    @GetMapping(value = "{catalogId}/categories")
    public ResponseEntity<List<Category>> getCategories(@ApiParam(value = "catalog id", required = true) @PathVariable String catalogId,
                                                     @RequestParam(value = "startIndex", required = false) Integer startIndex,
                                                     @RequestParam(value = "pageSize", required = false) Integer pageSize,
                                                     @RequestParam(value = "sortCol", required= false) String sortColumn,
                                                     @RequestParam(value = "sortDir", required = false) String sortDirection) {
        Type listType = new TypeToken<List<Category>>() {}.getType();
        List<Category> categories;
        if (startIndex == null || pageSize == null) {
            categories = categoryModelMapper.map(categoryService.getCategories(catalogId), listType);
        } else {
            sortDirection = validateSortDirection(sortDirection);
            if (sortColumn == null || sortColumn.length() == 0) {
                sortColumn = "name";
            }
            int startPage = 0;
            if (startIndex > 0) {
                startPage = startIndex / pageSize;
            }
            categories = categoryModelMapper.map(categoryService.getCategories(startPage, pageSize, sortColumn,
                    sortDirection), listType);
        }
        if (categories == null) {
            categories = new ArrayList<>();
        }
        return new ResponseEntity<>(categories, HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Create a catalog Category", notes = "Creates a catalog category", tags = {"Catalog"})
    @PostMapping(value = "/category", consumes=Versions.V1_0, produces=Versions.V1_0)
    public ResponseEntity<Category> createCategory(@ApiParam(value = "category", required = true) @RequestBody Category category) {
        if (category.getCatalogId() == null) {
            throw new IllegalArgumentException("A catalog id is required to create a category.");
        }
        if (DEFAULT_CATALOG.equals(category.getCatalogId())) {
            throw new IllegalArgumentException("The default catalog cannot be modified at run time.");
        }
        Optional<CategoryModel> opt = categoryService.getCategory(category.getCatalogId(), category.getName());
        if (opt != null && opt.isPresent()) {
            throw new IllegalStateException("A category named "+ category.getName() + " in catalog " +
                    category.getCatalogId() + " already exists");
        }
        CategoryModel model = categoryConverter.convert(category);
        model = categoryService.saveCategory(model);
        return new ResponseEntity<>(categoryModelConverter.convert(model), HttpStatus.CREATED);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Update a catalog Category", notes = "Updates a catalog category", tags = {"Catalog"})
    @PutMapping(value = "/category", consumes=Versions.V1_0, produces=Versions.V1_0)
    public ResponseEntity<Category> updateCategory(@ApiParam(value = "category", required = true) @RequestBody Category category) {
        if (category.getCatalogId() == null) {
            throw new IllegalArgumentException("A catalog id is required to create a category.");
        }
        if (DEFAULT_CATALOG.equals(category.getCatalogId())) {
            throw new IllegalArgumentException("The default catalog cannot be modified at run time.");
        }
        CategoryModel model = categoryConverter.convert(category);
        model = categoryService.saveCategory(model);
        return new ResponseEntity<>(categoryModelConverter.convert(model), HttpStatus.OK);
    }

    @ApiImplicitParams( {@ApiImplicitParam(dataType = "String", name = "Authorization", paramType = "header")})
    @ApiOperation(value = "Deletes a catalog category", notes = "Deletes a catalog category", tags = {"Catalog"})
    @DeleteMapping(value = "/category/{id}")
    public ResponseEntity<?> deleteCategory(@RequestParam("id") Long categoryId) {
        categoryService.deleteCategory(categoryId);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    private String validateSortDirection(String sortDirection) {
        if (sortDirection == null) {
            sortDirection = "ASC";
        } else if (!sortDirection.equals("ASC") && !sortDirection.equals("DESC")) {
            LOGGER.warn("Invalid sort direction {}, defaulting to ascending", sortDirection);
            sortDirection = "ASC";
        }
        return sortDirection;
    }
}