/**
 * Created by mirco on 04.12.15.
 */

package eu.hbp.mip.controllers;

import com.github.slugify.Slugify;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import eu.hbp.mip.configuration.SecurityConfiguration;
import eu.hbp.mip.model.Model;
import eu.hbp.mip.model.User;
import eu.hbp.mip.model.Variable;
import eu.hbp.mip.repositories.*;
import eu.hbp.mip.utils.DataUtil;
import io.swagger.annotations.*;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.*;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@RestController
@RequestMapping(value = "/models", produces = {APPLICATION_JSON_VALUE})
@Api(value = "/models", description = "the models API")
public class ModelsApi {

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

    @Autowired
    private SecurityConfiguration securityConfiguration;

    @Autowired
    private DatasetRepository datasetRepository;

    @Autowired
    private ModelRepository modelRepository;

    @Autowired
    private QueryRepository queryRepository;

    @Autowired
    private ConfigRepository configRepository;

    @Autowired
    private VariableRepository variableRepository;

    @Autowired
    @Qualifier("dataUtil")
    private DataUtil dataUtil;


    @ApiOperation(value = "Get models", response = Model.class, responseContainer = "List")
    @RequestMapping(method = RequestMethod.GET)
    public ResponseEntity<List> getModels(
            @ApiParam(value = "Only ask own models") @RequestParam(value = "own", required = false) Boolean own,
            @ApiParam(value = "Only ask published models") @RequestParam(value = "valid", required = false) Boolean valid
    )  {
        LOGGER.info("Get models");

        User user = securityConfiguration.getUser();

        Iterable<Model> models;
        if(own != null && own)
        {
            models = modelRepository.findByCreatedByOrderByCreatedAt(user);
        }
        else
        {
            models = modelRepository.findByValidOrCreatedByOrderByCreatedAt(true, user);
        }

        if(valid != null && models != null)
        {
            for (Iterator<Model> i = models.iterator(); i.hasNext(); )
            {
                Model m = i.next();
                if(valid != m.getValid())
                {
                    i.remove();
                }
            }
        }

        List<Object> modelsList = new LinkedList<>();
        models = models != null ? models : new LinkedList<>();
        for (Model m : models) {
            m.setDataset(datasetRepository.findOne(m.getDataset().getCode()));
            modelsList.add(getModelWithDataset(m));
        }

        return ResponseEntity.ok(modelsList);
    }


    @ApiOperation(value = "Create a model", response = Model.class)
    @ApiResponses(value = { @ApiResponse(code = 201, message = "Model created") })
    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity<Model> addAModel(
            @RequestBody @ApiParam(value = "Model to create", required = true) Model model
    )  {

        LOGGER.info("Create a model");

        User user = securityConfiguration.getUser();

        model.setTitle(model.getConfig().getTitle().get("text"));
        model.setCreatedBy(user);
        model.setCreatedAt(new Date());
        if(model.getValid() == null)
        {
            model.setValid(false);
        }

        ensureTitleUniqueness(model);
        ensureSlugUniqueness(model);

        Map<String, String> map = new HashMap<>(model.getConfig().getTitle());
        map.put("text", model.getTitle());
        model.getConfig().setTitle(map);

        saveVariables(model.getQuery().getVariables());
        saveVariables(model.getQuery().getCovariables());
        saveVariables(model.getQuery().getGrouping());

        configRepository.save(model.getConfig());
        queryRepository.save(model.getQuery());
        datasetRepository.save(model.getDataset());
        modelRepository.save(model);

        LOGGER.info("Model saved (also saved model.config and model.query)");

        return ResponseEntity.status(HttpStatus.CREATED).body(model);
    }

    private void saveVariables(@RequestBody @ApiParam(value = "Model to create", required = true) List<Variable> variables) {
        for (Variable var : variables)
        {
            variableRepository.save(var);
        }
    }

    private void ensureSlugUniqueness(@RequestBody @ApiParam(value = "Model to create", required = true) Model model) {
        String slug = createSlug(model.getTitle());
        boolean slugExists = true;
        for(int i = 1; slugExists; i++)
        {
            slugExists = modelRepository.exists(slug);
            if(slugExists)
            {
                if(i > 1)
                {
                    slug = slug.substring(0, slug.length()-2);
                }
                slug += "-"+i;
            }
            model.setSlug(slug);
        }
    }

    private String createSlug(@RequestBody @ApiParam(value = "Model to create", required = true) String title) {
        String slug;
        try {
            slug = new Slugify().slugify(title);
        } catch (IOException e) {
            slug = "";  // Should never happen
            LOGGER.trace(e);
        }
        return slug;
    }

    private void ensureTitleUniqueness(@RequestBody @ApiParam(value = "Model to create", required = true) Model model) {
        boolean titleExists = true;
        for(int i = 1; titleExists; i++)
        {
            String title = model.getTitle();
            titleExists = modelRepository.countByTitle(title) > 0;
            if(titleExists)
            {
                if(i > 1)
                {
                    title = title.substring(0, title.length()-4);
                }
                model.setTitle(title + " (" + i + ")");
            }
        }
    }

    @ApiOperation(value = "Get a model", response = Model.class)
    @RequestMapping(value = "/{slug}", method = RequestMethod.GET)
    public ResponseEntity<Model> getAModel(
            @ApiParam(value = "slug", required = true) @PathVariable("slug") String slug
    )  {
        LOGGER.info("Get a model");

        User user = securityConfiguration.getUser();

        Model model = modelRepository.findOne(slug);

        if(model == null)
        {
            LOGGER.warn("Cannot find model : " + slug);
            return ResponseEntity.badRequest().body(null);
        }

        if (!model.getValid() && !model.getCreatedBy().getUsername().equals(user.getUsername()))
        {
            return new ResponseEntity<>(HttpStatus.FORBIDDEN);
        }

        List<String> yAxisVars = configRepository.findOne(model.getConfig().getId()).getyAxisVariables();
        Collection<String> yAxisVarsColl = new LinkedHashSet<>(yAxisVars);
        model.getConfig().setyAxisVariables(new LinkedList<>(yAxisVarsColl));

        return ResponseEntity.ok(getModelWithDataset(model));
    }


    @ApiOperation(value = "Update a model", response = Void.class)
    @ApiResponses(value = { @ApiResponse(code = 204, message = "Model updated") })
    @RequestMapping(value = "/{slug}", method = RequestMethod.PUT)
    public ResponseEntity<Void> updateAModel(
            @ApiParam(value = "slug", required = true) @PathVariable("slug") String slug,
            @RequestBody @ApiParam(value = "Model to update", required = true) Model model
    )  {
        LOGGER.info("Update a model");

        User user = securityConfiguration.getUser();
        Model oldModel = modelRepository.findOne(slug);

        if(!user.getUsername().equals(oldModel.getCreatedBy().getUsername()))
        {
            return new ResponseEntity<>(HttpStatus.FORBIDDEN);
        }

        model.setTitle(model.getConfig().getTitle().get("text"));

        String oldTitle = oldModel.getTitle();
        String newTitle = model.getTitle();

        // If title has been updated, ensure it is unique
        if(!newTitle.equals(oldTitle)) {
            boolean newTitleExists = true;
            for(int i = 1; newTitleExists && !newTitle.equals(oldTitle); i++)
            {
                newTitle = model.getTitle();
                newTitleExists = modelRepository.countByTitle(newTitle) > 0;
                if (newTitleExists && !newTitle.equals(oldTitle)) {
                    if (i > 1) {
                        newTitle = newTitle.substring(0, newTitle.length() - 4);
                    }
                    model.setTitle(newTitle + " (" + i + ")");
                }
            }
        }

        Map<String, String> map = new HashMap<>(model.getConfig().getTitle());
        map.put("text", model.getTitle());
        model.getConfig().setTitle(map);

        configRepository.save(model.getConfig());
        queryRepository.save(model.getQuery());
        datasetRepository.save(model.getDataset());
        modelRepository.save(model);

        LOGGER.info("Model updated (also saved/updated model.config and model.query)");

        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

    private Model getModelWithDataset(Model model)
    {
        List<String> allVars = new LinkedList<>();
        allVars.addAll(model.getDataset().getVariable());
        allVars.addAll(model.getDataset().getHeader());
        allVars.addAll(model.getDataset().getGrouping());

        Gson gson = new Gson();
        JsonObject jsonModel = gson.fromJson(gson.toJson(model, Model.class), JsonObject.class);
        jsonModel.get("dataset").getAsJsonObject()
                .add("data", dataUtil.getDataFromVariables(allVars));

        return gson.fromJson(jsonModel, Model.class);
    }

}