package com.clevergang.dbtests.repository.impl.ebean;

import com.avaje.ebean.Ebean;
import com.avaje.ebean.EbeanServer;
import com.avaje.ebean.SqlRow;
import com.clevergang.dbtests.repository.api.DataRepository;
import com.clevergang.dbtests.repository.api.data.*;
import com.clevergang.dbtests.repository.impl.ebean.entities.CompanyEntity;
import com.clevergang.dbtests.repository.impl.ebean.entities.DepartmentEntity;
import com.clevergang.dbtests.repository.impl.ebean.entities.EmployeeEntity;
import com.clevergang.dbtests.repository.impl.ebean.entities.ProjectEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.stream.Collectors;

/**
 * EBean implementation of DataRepository interface...
 *
 * @author Bretislav Wajtr
 */
@Repository
public class EBeanDataRepositoryImpl implements DataRepository {

    private final EbeanServer ebean;

    @Autowired
    public EBeanDataRepositoryImpl(EbeanServer ebean) {
        this.ebean = ebean;
    }

    @Override
    public Company findCompany(Integer pid) {
        CompanyEntity companyEntity = ebean.find(CompanyEntity.class, pid);

        Company result = new Company();
        result.setPid(companyEntity.getPid());
        result.setName(companyEntity.getName());
        result.setAddress(companyEntity.getAddress());
        return result;
    }

    @Override
    public Company findCompanyUsingSimpleStaticStatement(Integer pid) {
        // I didn't find a way how to setup EBean so the query is executed statically (using JDBC regular Statement
        // not PreparedStatement). However, EBean is polite and provides way how to access the underlying JDBC connection
        // -> therefore we can actually fall back to JDBC way of doing things (which is not such a big deal since we need simple
        // Statements only rarely):
        try {
            String query = "SELECT pid, address, name " +
                    "FROM company " +
                    "WHERE pid = " + pid;

            Connection connection = ebean.currentTransaction().getConnection();
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery(query);
            Company result = null;
            if (resultSet.next()) {
                result = new Company();
                result.setPid(resultSet.getInt("pid"));
                result.setAddress(resultSet.getString("address"));
                result.setName(resultSet.getString("name"));

            }
            return result;
        } catch (SQLException e) {
            // just wrap checked exceptions as e
            throw new RuntimeException(e);
        }
    }

    @Override
    public void removeProject(Integer pid) {
        ebean.delete(ProjectEntity.class, pid);
    }

    @Override
    public Department findDepartment(Integer pid) {
        DepartmentEntity departmentEntity = ebean.find(DepartmentEntity.class, pid);

        Department result = new Department();
        result.setPid(departmentEntity.getPid());
        result.setName(departmentEntity.getName());
        result.setCompanyPid(departmentEntity.getCompanyPid());
        return result;
    }

    @Override
    public List<Department> findDepartmentsOfCompany(Company company) {
        List<DepartmentEntity> departments = ebean
                .find(DepartmentEntity.class)
                .where()
                .eq("companyPid", company.getPid())
                .orderBy("pid")
                .findList();

        return departments.stream()
                .map(entity -> {
                    Department department = new Department();
                    department.setPid(entity.getPid());
                    department.setName(entity.getName());
                    department.setCompanyPid(entity.getCompanyPid());
                    return department;
                }).collect(Collectors.toList());
    }

    @Override
    public void deleteDepartments(List<Department> departmentsToDelete) {
        List<Integer> ids = departmentsToDelete.stream().map(Department::getPid).collect(Collectors.toList());
        ebean.deleteAll(DepartmentEntity.class, ids);
    }

    @Override
    public void updateDepartments(List<Department> departmentsToUpdate) {
        List<DepartmentEntity> entities = departmentsToUpdate.stream()
                .map(department -> {
                    DepartmentEntity entity = new DepartmentEntity();
                    entity.setPid(department.getPid());
                    entity.setName(department.getName());
                    entity.setCompanyPid(department.getCompanyPid());
                    return entity;
                }).collect(Collectors.toList());

        ebean.updateAll(entities);
    }

    @Override
    public void insertDepartments(List<Department> departmentsToInsert) {
        List<DepartmentEntity> entities = departmentsToInsert.stream()
                .map(department -> {
                    DepartmentEntity entity = new DepartmentEntity();
                    entity.setName(department.getName());
                    entity.setCompanyPid(department.getCompanyPid());
                    return entity;
                }).collect(Collectors.toList());

        // Please see comment about batching in insertProjects() method
        ebean.saveAll(entities);
    }

    @Override
    public Project findProject(Integer pid) {
        ProjectEntity projectEntity = ebean.find(ProjectEntity.class, pid);

        Project result = new Project();
        result.setPid(projectEntity.getPid());
        result.setName(projectEntity.getName());
        result.setDate(projectEntity.getDateStarted());
        return result;
    }

    @Override
    public Integer insertProject(Project project) {
        ProjectEntity entity = new ProjectEntity();

        entity.setName(project.getName());
        entity.setDateStarted(project.getDate());

        ebean.save(entity);
        return entity.getPid();
    }

    @Override
    public List<Integer> insertProjects(List<Project> projects) {
        List<ProjectEntity> entities = projects.stream()
                .map(project -> {
                    ProjectEntity entity = new ProjectEntity();
                    entity.setName(project.getName());
                    entity.setDateStarted(project.getDate());
                    return entity;
                }).collect(Collectors.toList());

        // Actually EBean handles batch inserts/updates in a very clever way. The default setting for batching is
        // following:
        //    ebean.currentTransaction().getBatch() == PersistBatch.NONE
        //    ebean.currentTransaction().getBatchOnCascade() ==  PersistBatch.ALL
        // so basically when you do single insert (ebean.save()) then batching is not used. But when
        // use use ebean.saveAll, then value for BatchOnCascade is used for storing the value and in
        // our case that's PersistBatch.ALL -> batching is used.
        // That means, for default settings:
        //   ebean.save()  --> no batching is used
        //   ebean.saveAll()  --> batching is automatically used
        // WHICH IS AWESOME!
        ebean.saveAll(entities);

        return entities.stream().map(ProjectEntity::getPid).collect(Collectors.toList());
    }

    @Override
    public List<ProjectsWithCostsGreaterThanOutput> getProjectsWithCostsGreaterThan(int totalCostBoundary) {
        String query;
        query = "WITH project_info AS (\n" +
                "    SELECT project.pid project_pid, project.name project_name, salary monthly_cost, company.name company_name\n" +
                "    FROM project\n" +
                "      JOIN projectemployee ON project.pid = projectemployee.project_pid\n" +
                "      JOIN employee ON projectemployee.employee_pid = employee.pid\n" +
                "      LEFT JOIN department ON employee.department_pid = department.pid\n" +
                "      LEFT JOIN company ON department.company_pid = company.pid\n" +
                "),\n" +
                "project_cost AS (\n" +
                "    SELECT project_pid, sum(monthly_cost) total_cost\n" +
                "    FROM project_info GROUP BY project_pid\n" +
                ")\n" +
                "SELECT project_name, total_cost, company_name, sum(monthly_cost) company_cost FROM project_info\n" +
                "  JOIN project_cost USING (project_pid)\n" +
                "WHERE total_cost > :totalCostBoundary\n" +
                "GROUP BY project_name, total_cost, company_name\n" +
                "ORDER BY company_name";

        return ebean.createSqlQuery(query)
                .setParameter("totalCostBoundary", totalCostBoundary)
                .findList()
                .stream()
                .map(row -> {
                    ProjectsWithCostsGreaterThanOutput output = new ProjectsWithCostsGreaterThanOutput();
                    output.setProjectName(row.getString("project_name"));
                    output.setTotalCost(row.getBigDecimal("total_cost"));
                    output.setCompanyName(row.getString("company_name"));
                    output.setCompanyCost(row.getBigDecimal("company_cost"));
                    return output;
                })
                .collect(Collectors.toList());
    }

    @Override
    public Employee findEmployee(Integer pid) {
        EmployeeEntity entity = ebean.find(EmployeeEntity.class, pid);

        Employee result = new Employee();
        result.setPid(entity.getPid());
        result.setName(entity.getName());
        result.setDepartmentPid(entity.getDepartmentPid());
        result.setEmail(entity.getEmail());
        result.setSalary(entity.getSalary());
        result.setSurname(entity.getSurname());

        return result;
    }

    @Override
    public List<Employee> employeesWithSalaryGreaterThan(Integer minSalary) {
        return ebean
                .find(EmployeeEntity.class)
                .where()
                .gt("salary", minSalary)
                .findList() // query executed here
                .stream()
                .map(entity -> {
                    Employee emp = new Employee();
                    emp.setPid(entity.getPid());
                    emp.setSurname(entity.getSurname());
                    emp.setName(entity.getName());
                    emp.setSalary(entity.getSalary());
                    emp.setEmail(entity.getEmail());
                    emp.setDepartmentPid(entity.getDepartmentPid());
                    return emp;
                }).collect(Collectors.toList());
    }

    @Override
    public void updateEmployee(Employee employeeToUpdate) {
        EmployeeEntity entity = new EmployeeEntity();
        entity.setPid(employeeToUpdate.getPid());
        entity.setEmail(employeeToUpdate.getEmail());
        entity.setName(employeeToUpdate.getName());
        entity.setSurname(employeeToUpdate.getSurname());
        entity.setSalary(employeeToUpdate.getSalary());
        entity.setDepartmentPid(employeeToUpdate.getDepartmentPid());

        ebean.update(entity);
    }

    @Override
    public RegisterEmployeeOutput callRegisterEmployee(String name, String surname, String email, BigDecimal salary, String departmentName, String companyName) {
        String query = "SELECT employee_id, department_id, company_id FROM register_employee(:name, :surname, :email, :salary, :departmentName, :companyName)";

        SqlRow row = ebean.createSqlQuery(query)
                .setParameter("name", name)
                .setParameter("surname", surname)
                .setParameter("email", email)
                .setParameter("salary", salary)
                .setParameter("departmentName", departmentName)
                .setParameter("companyName", companyName)
                .findUnique();

        RegisterEmployeeOutput result = new RegisterEmployeeOutput();
        result.setEmployeePid(row.getInteger("employee_id"));
        result.setDepartmentPid(row.getInteger("department_id"));
        result.setCompanyPid(row.getInteger("company_id"));

        // this is required so the EBean caches are invalidated
        Ebean.externalModification("department", true, false, false);
        Ebean.externalModification("company", true, false, false);
        Ebean.externalModification("employee", true, false, false);

        return result;
    }

    @Override
    public Integer getProjectsCount() {
        return ebean.find(ProjectEntity.class).findCount();
    }
}