/*
 * Copyright (c) 2018 Sony Pictures Imageworks Inc.
 *
 * Licensed 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 com.imageworks.spcue.dao.oracle;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.CallableStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

import com.imageworks.spcue.AllocationInterface;
import com.imageworks.spcue.DispatchHost;
import com.imageworks.spcue.EntityCreationError;
import com.imageworks.spcue.HostEntity;
import com.imageworks.spcue.HostInterface;
import com.imageworks.spcue.LocalHostAssignment;
import com.imageworks.spcue.Source;
import com.imageworks.spcue.dao.HostDao;
import com.imageworks.spcue.dispatcher.Dispatcher;
import com.imageworks.spcue.dispatcher.ResourceReservationFailureException;
import com.imageworks.spcue.grpc.host.HardwareState;
import com.imageworks.spcue.grpc.host.HostTagType;
import com.imageworks.spcue.grpc.host.LockState;
import com.imageworks.spcue.grpc.host.ThreadMode;
import com.imageworks.spcue.grpc.report.HostReport;
import com.imageworks.spcue.grpc.report.RenderHost;
import com.imageworks.spcue.util.CueUtil;
import com.imageworks.spcue.util.SqlUtil;


public class HostDaoJdbc extends JdbcDaoSupport implements HostDao {

    public static final RowMapper<HostEntity> HOST_DETAIL_MAPPER = new RowMapper<HostEntity>() {
        public HostEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
            HostEntity host = new HostEntity();
            host.facilityId = rs.getString("pk_facility");
            host.allocId = rs.getString("pk_alloc");
            host.id = rs.getString("pk_host");
            host.lockState = LockState.valueOf(rs.getString("str_lock_state"));
            host.name = rs.getString("str_name");
            host.nimbyEnabled = rs.getBoolean("b_nimby");
            host.state = HardwareState.valueOf(rs.getString("str_state"));
            host.unlockAtBoot = rs.getBoolean("b_unlock_boot");
            host.cores = rs.getInt("int_cores");
            host.idleCores = rs.getInt("int_cores_idle");
            host.memory = rs.getInt("int_mem");
            host.idleMemory = rs.getInt("int_mem_idle");
            host.gpu = rs.getInt("int_gpu");
            host.idleGpu = rs.getInt("int_gpu_idle");
            host.dateBooted = rs.getDate("ts_booted");
            host.dateCreated = rs.getDate("ts_created");
            host.datePinged = rs.getDate("ts_ping");
            return host;
        }
    };

    public static final RowMapper<HostInterface> HOST_MAPPER = new RowMapper<HostInterface>() {
        public HostInterface mapRow(final ResultSet rs, int rowNum) throws SQLException {
            return new HostInterface() {
                final String id = rs.getString("pk_host");
                final String allocid =  rs.getString("pk_alloc");
                final String name = rs.getString("str_name");
                final String facility =  rs.getString("pk_facility");

                public String getHostId() { return id; }
                public String getAllocationId() { return allocid; }
                public String getId() { return id; }
                public String getName() { return name; }
                public String getFacilityId() { return facility; };
            };
        }
    };

    private static final String GET_HOST_DETAIL =
        "SELECT " +
            "host.pk_host, " +
            "host.pk_alloc,"+
            "host.str_lock_state,"+
            "host.b_nimby,"+
            "host.b_unlock_boot,"+
            "host.int_cores,"+
            "host.int_cores_idle,"+
            "host.int_mem,"+
            "host.int_mem_idle,"+
            "host.int_gpu,"+
            "host.int_gpu_idle,"+
            "host.ts_created,"+
            "host.str_name, " +
            "host_stat.str_state,"+
            "host_stat.ts_ping,"+
            "host_stat.ts_booted, "+
            "alloc.pk_facility " +
        "FROM " +
            "host, " +
            "alloc, " +
            "host_stat " +
        "WHERE " +
            "host.pk_host = host_stat.pk_host " +
        "AND " +
            "host.pk_alloc = alloc.pk_alloc ";

    @Override
    public void lockForUpdate(HostInterface host) {
        try {
            getJdbcTemplate().queryForObject(
                    "SELECT pk_host FROM host WHERE pk_host=? " +
                    "FOR UPDATE NOWAIT",
                    String.class, host.getHostId());
        } catch (Exception e) {
            throw new ResourceReservationFailureException("unable to lock host " +
                    host.getName() + ", the host was locked by another thread.", e);
        }
    }

    @Override
    public HostEntity getHostDetail(HostInterface host) {
        return getJdbcTemplate().queryForObject(GET_HOST_DETAIL + " AND host.pk_host=?",
                HOST_DETAIL_MAPPER, host.getHostId());
    }

    @Override
    public HostEntity getHostDetail(String id) {
        return getJdbcTemplate().queryForObject(GET_HOST_DETAIL + " AND host.pk_host=?",
                HOST_DETAIL_MAPPER, id);
    }

    @Override
    public HostEntity findHostDetail(String name) {
        return getJdbcTemplate().queryForObject(GET_HOST_DETAIL + " AND host.str_name=?",
                HOST_DETAIL_MAPPER, name);
    }

    private static final String GET_HOST=
        "SELECT " +
            "host.pk_host, " +
            "host.pk_alloc,"+
            "host.str_name, " +
            "alloc.pk_facility " +
        "FROM " +
            "host," +
            "alloc " +
        "WHERE " +
            "host.pk_alloc = alloc.pk_alloc " ;

    @Override
    public HostInterface getHost(String id) {
        return getJdbcTemplate().queryForObject(GET_HOST + " AND host.pk_host=?",
                HOST_MAPPER, id);
    }

    @Override
    public HostInterface getHost(LocalHostAssignment l) {
        return getJdbcTemplate().queryForObject(GET_HOST + " AND host.pk_host = ("+
                "SELECT pk_host FROM host_local WHERE pk_host_local=?)",
                HOST_MAPPER, l.getId());
    }

    @Override
    public HostInterface findHost(String name) {
        return getJdbcTemplate().queryForObject(
                GET_HOST + " AND (host.str_name=? OR host.str_fqdn=?)",
                HOST_MAPPER, name, name);
    }

    public static final RowMapper<DispatchHost> DISPATCH_HOST_MAPPER =
        new RowMapper<DispatchHost>() {
        public DispatchHost mapRow(ResultSet rs, int rowNum) throws SQLException {
            DispatchHost host = new DispatchHost();
            host.id = rs.getString("pk_host");
            host.allocationId = rs.getString("pk_alloc");
            host.facilityId = rs.getString("pk_facility");
            host.name = rs.getString("str_name");
            host.lockState = LockState.valueOf(rs.getString("str_lock_state"));
            host.memory = rs.getInt("int_mem");
            host.cores = rs.getInt("int_cores");
            host.gpu= rs.getInt("int_gpu");
            host.idleMemory= rs.getInt("int_mem_idle");
            host.idleCores = rs.getInt("int_cores_idle");
            host.idleGpu= rs.getInt("int_gpu_idle");
            host.isNimby = rs.getBoolean("b_nimby");
            host.threadMode = rs.getInt("int_thread_mode");
            host.tags = rs.getString("str_tags");
            host.os = rs.getString("str_os");
            host.hardwareState =
                HardwareState.valueOf(rs.getString("str_state"));
            return host;
        }
    };

    public static final String GET_DISPATCH_HOST =
        "SELECT " +
            "host.pk_host,"+
            "host.pk_alloc,"+
            "host.str_name," +
            "host.str_lock_state, " +
            "host.int_cores, "+
            "host.int_cores_idle, " +
            "host.int_mem,"+
            "host.int_mem_idle, "+
            "host.int_gpu,"+
            "host.int_gpu_idle, "+
            "host.b_nimby, "+
            "host.int_thread_mode, "+
            "host.str_tags, " +
            "host_stat.str_os, " +
            "host_stat.str_state, " +
            "alloc.pk_facility " +
        "FROM " +
            "host " +
        "INNER JOIN host_stat " +
            "ON (host.pk_host = host_stat.pk_host) " +
        "INNER JOIN alloc " +
            "ON (host.pk_alloc = alloc.pk_alloc) ";

    @Override
    public DispatchHost findDispatchHost(String name) {
        try {
            return getJdbcTemplate().queryForObject(
                    GET_DISPATCH_HOST +
                    "WHERE (host.str_name=? OR host.str_fqdn=?)",
                    DISPATCH_HOST_MAPPER, name, name);
        } catch (EmptyResultDataAccessException e) {
            throw new EmptyResultDataAccessException(
                    "Failed to find host " + name, 1);
        }
    }

    @Override
    public DispatchHost getDispatchHost(String id) {
        return getJdbcTemplate().queryForObject(
                GET_DISPATCH_HOST +
                "WHERE host.pk_host=?",
                DISPATCH_HOST_MAPPER, id);
    }

    private static final String[] INSERT_HOST_DETAIL =
    {
        "INSERT INTO " +
            "host " +
        "("+
            "pk_host, " +
            "pk_alloc, " +
            "str_name, " +
            "b_nimby, " +
            "str_lock_state, " +
            "int_procs,"+
            "int_cores, " +
            "int_cores_idle, " +
            "int_mem,"+
            "int_mem_idle,"+
            "int_gpu,"+
            "int_gpu_idle,"+
            "str_fqdn, " +
            "int_thread_mode "+
        ") " +
        "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",

        "INSERT INTO " +
        "host_stat " +
        "("+
            "pk_host_stat," +
            "pk_host,"+
            "int_mem_total, " +
            "int_mem_free,"+
            "int_gpu_total, " +
            "int_gpu_free,"+
            "int_swap_total, " +
            "int_swap_free,"+
            "int_mcp_total, " +
            "int_mcp_free,"+
            "int_load, " +
            "ts_booted, " +
            "str_state, " +
            "str_os " +
        ") "+
        "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)"

    };

    @Override
    public void insertRenderHost(RenderHost host, AllocationInterface a, boolean useLongNames) {

        ThreadMode threadMode = ThreadMode.AUTO;
        if (host.getNimbyEnabled()) {
            threadMode = ThreadMode.ALL;
        }

        long memUnits = convertMemoryUnits(host);
        if (memUnits < Dispatcher.MEM_RESERVED_MIN) {
            throw new EntityCreationError("could not create host " + host.getName() + ", " +
                    " must have at least " + Dispatcher.MEM_RESERVED_MIN + " free memory.");
        }

        String fqdn;
        String name = host.getName();
        try {
            fqdn = InetAddress.getByName(host.getName()).getCanonicalHostName();
            // If the provided host name matches the pinged name, use the pinged name.
            // Otherwise use the provided name.
            // If the host lookup fails, use the provided name.
            // In all cases attempt to strip off the domain when setting the name.
            if (fqdn.equals(host.getName())) {
                name = getHostNameFromFQDN(fqdn, useLongNames);
            }
            else {
                name = getHostNameFromFQDN(host.getName(), useLongNames);
                fqdn = host.getName();
            }
        } catch (UnknownHostException e) {
            logger.warn(e);
            fqdn = host.getName();
            name = getHostNameFromFQDN(name, useLongNames);
        }

        String hid = SqlUtil.genKeyRandom();
        int coreUnits = host.getNumProcs() * host.getCoresPerProc();
        String os = host.getAttributes().get("SP_OS");
        if (os == null) {
            os = Dispatcher.OS_DEFAULT;
        }

        long totalGpu;
        if (host.getAttributes().containsKey("totalGpu"))
            totalGpu = Integer.parseInt(host.getAttributes().get("totalGpu"));
        else
            totalGpu = 0;

        long freeGpu;
        if (host.getAttributes().containsKey("freeGpu"))
            freeGpu = Integer.parseInt(host.getAttributes().get("freeGpu"));
        else
            freeGpu = 0;

        getJdbcTemplate().update(INSERT_HOST_DETAIL[0],
                hid, a.getAllocationId(), name, host.getNimbyEnabled(),
                LockState.OPEN.toString(), host.getNumProcs(), coreUnits, coreUnits,
                memUnits, memUnits, totalGpu, totalGpu,
                fqdn, threadMode.getNumber());

        getJdbcTemplate().update(INSERT_HOST_DETAIL[1],
                hid, hid, host.getTotalMem(), host.getFreeMem(),
                totalGpu, freeGpu,
                host.getTotalSwap(), host.getFreeSwap(),
                host.getTotalMcp(), host.getFreeMcp(),
                host.getLoad(), new Timestamp(host.getBootTime() * 1000l),
                host.getState().toString(), os);
    }

    @Override
    public void recalcuateTags(final String id) {
        getJdbcTemplate().call(new CallableStatementCreator() {
            public CallableStatement createCallableStatement(Connection con) throws SQLException {
                CallableStatement c = con.prepareCall("{ call recalculate_tags(?) }");
                c.setString(1, id);
                return c;
            }
        }, new ArrayList<SqlParameter>());
    }

    private static final String UPDATE_RENDER_HOST =
        "UPDATE " +
            "host_stat " +
        "SET " +
            "int_mem_total=?, " +
            "int_mem_free=?, " +
            "int_swap_total=?, " +
            "int_swap_free=?, "+
            "int_mcp_total=?, " +
            "int_mcp_free=?, " +
            "int_gpu_total=?, " +
            "int_gpu_free=?, " +
            "int_load=?," +
            "ts_booted = ?,  " +
            "ts_ping = systimestamp, "+
            "str_os=? " +
        "WHERE " +
            "pk_host=?";

    @Override
    public void updateHostStats(HostInterface host,
            long totalMemory, long freeMemory,
            long totalSwap, long freeSwap,
            long totalMcp, long freeMcp,
            long totalGpu, long freeGpu,
            int load, Timestamp bootTime,
            String os) {

        if (os == null) {
            os = Dispatcher.OS_DEFAULT;
        }

        getJdbcTemplate().update(UPDATE_RENDER_HOST,
                totalMemory, freeMemory, totalSwap,
                freeSwap, totalMcp, freeMcp, totalGpu, freeGpu, load,
                bootTime, os, host.getHostId());
    }

    @Override
    public boolean hostExists(String hostname) {
        try {
            return getJdbcTemplate().queryForObject(
                    "SELECT 1 FROM host WHERE (str_fqdn=? OR str_name=?)",
                    Integer.class, hostname, hostname) > 0;
        } catch (EmptyResultDataAccessException e) {
            return false;
        }
    }

    @Override
    public void updateHostResources(HostInterface host, HostReport report) {

        long memory = convertMemoryUnits(report.getHost());
        int cores = report.getHost().getNumProcs() * report.getHost().getCoresPerProc();

        long totalGpu;
        if (report.getHost().getAttributes().containsKey("totalGpu"))
            totalGpu = Integer.parseInt(report.getHost().getAttributes().get("totalGpu"));
        else
            totalGpu = 0;

        getJdbcTemplate().update(
                "UPDATE " +
                    "host " +
                "SET " +
                    "b_nimby=?,"+
                    "int_cores=?," +
                    "int_cores_idle=?," +
                    "int_mem=?," +
                    "int_mem_idle=?, " +
                    "int_gpu=?," +
                    "int_gpu_idle=? " +
                "WHERE " +
                    "pk_host=? "+
                "AND " +
                    "int_cores = int_cores_idle " +
                "AND " +
                    "int_mem = int_mem_idle",
                    report.getHost().getNimbyEnabled(), cores, cores,
                    memory, memory, totalGpu, totalGpu, host.getId());
    }

    @Override
    public void updateHostLock(HostInterface host, LockState state, Source source) {
        getJdbcTemplate().update(
                "UPDATE host SET str_lock_state=?, str_lock_source=? WHERE pk_host=?",
                state.toString(), source.toString(), host.getHostId());
    }

    @Override
    public void updateHostRebootWhenIdle(HostInterface host, boolean enabled) {
        getJdbcTemplate().update("UPDATE host SET b_reboot_idle=? WHERE pk_host=?",
                enabled, host.getHostId());
    }

    @Override
    public void deleteHost(HostInterface host) {
        getJdbcTemplate().update(
                "DELETE FROM comments WHERE pk_host=?",host.getHostId());
        getJdbcTemplate().update(
                "DELETE FROM host WHERE pk_host=?",host.getHostId());
    }

    @Override
    public void updateHostState(HostInterface host, HardwareState state) {
        getJdbcTemplate().update(
                "UPDATE host_stat SET str_state=? WHERE pk_host=?",
                state.toString(), host.getHostId());
    }

    @Override
    public void updateHostSetAllocation(HostInterface host, AllocationInterface alloc) {

        String tag = getJdbcTemplate().queryForObject(
                "SELECT str_tag FROM alloc WHERE pk_alloc=?",
                String.class, alloc.getAllocationId());
        getJdbcTemplate().update(
                "UPDATE host SET pk_alloc=? WHERE pk_host=?",
                alloc.getAllocationId(), host.getHostId());

        removeTagsByType(host, HostTagType.ALLOC);
        tagHost(host, tag, HostTagType.ALLOC);
    }

    @Override
    public boolean isHostLocked(HostInterface host) {
        return getJdbcTemplate().queryForObject(
                "SELECT COUNT(1) FROM host WHERE pk_host=? AND str_lock_state!=?",
                Integer.class, host.getHostId(), LockState.OPEN.toString()) > 0;
    }

    private static final String INSERT_TAG =
        "INSERT INTO " +
            "host_tag " +
        "(" +
            "pk_host_tag,"+
            "pk_host,"+
            "str_tag,"+
            "str_tag_type, " +
            "b_constant " +
        ") VALUES (?,?,?,?,?)";


    @Override
    public void tagHost(String id, String tag, HostTagType type) {
        boolean constant = false;
        if (type.equals(HostTagType.ALLOC))
            constant = true;

        getJdbcTemplate().update(INSERT_TAG,
                SqlUtil.genKeyRandom(), id, tag.trim(), type.toString(), constant);
    }

    @Override
    public void tagHost(HostInterface host, String tag, HostTagType type) {
        tagHost(host.getHostId(), tag, type);
    }

    @Override
    public void removeTagsByType(HostInterface host, HostTagType type) {
        getJdbcTemplate().update("DELETE FROM host_tag WHERE pk_host=? AND str_tag_type=?",
                host.getHostId(), type.toString());
    }

    @Override
    public void removeTag(HostInterface host, String tag) {
        getJdbcTemplate().update(
                "DELETE FROM host_tag WHERE pk_host=? AND str_tag=? AND b_constant=0",
                host.getHostId(), tag);
    }

    @Override
    public void renameTag(HostInterface host, String oldTag, String newTag) {
        getJdbcTemplate().update(
                "UPDATE host_tag SET str_tag=? WHERE pk_host=? AND str_tag=? AND b_constant=0",
                newTag, host.getHostId(), oldTag);
    }

    @Override
    public void updateThreadMode(HostInterface host, ThreadMode mode) {
        getJdbcTemplate().update(
                "UPDATE host SET int_thread_mode=? WHERE pk_host=?",
                mode.getNumber(), host.getHostId());
    }

    @Override
    public void updateHostOs(HostInterface host, String os) {
        getJdbcTemplate().update(
                "UPDATE host_stat SET str_os=? WHERE pk_host=?",
                 os, host.getHostId());
    }

    @Override
    public boolean isKillMode(HostInterface h) {
        return getJdbcTemplate().queryForObject(
                "SELECT COUNT(1) FROM host_stat WHERE pk_host = ? " +
                "AND int_swap_total - int_swap_free > ? AND int_mem_free < ?",
                Integer.class, h.getHostId(), Dispatcher.KILL_MODE_SWAP_THRESHOLD,
                Dispatcher.KILL_MODE_MEM_THRESHOLD) > 0;
    }

    @Override
    public int getStrandedCoreUnits(HostInterface h) {
        try {
            int idle_cores =  getJdbcTemplate().queryForObject(
                    "SELECT int_cores_idle FROM host WHERE pk_host = ? AND int_mem_idle <= ?",
                    Integer.class, h.getHostId(),
                    Dispatcher.MEM_STRANDED_THRESHHOLD);
            return (int) (Math.floor(idle_cores / 100.0)) * 100;
        } catch (EmptyResultDataAccessException e) {
            return 0;
        }
    }

    private static final String IS_HOST_UP =
        "SELECT " +
            "COUNT(1) " +
        "FROM " +
            "host_stat "+
        "WHERE " +
            "host_stat.str_state = ? " +
        "AND " +
            "host_stat.pk_host = ? ";

    @Override
    public boolean isHostUp(HostInterface host) {
        return getJdbcTemplate().queryForObject(IS_HOST_UP,
                Integer.class, HardwareState.UP.toString(),
                host.getHostId()) == 1;
    }

    private static final String IS_PREFER_SHOW =
        "SELECT " +
            "COUNT(1) " +
        "FROM " +
            "host," +
            "owner," +
            "deed "+
        "WHERE " +
            "host.pk_host = deed.pk_host " +
        "AND " +
            "deed.pk_owner = owner.pk_owner " +
        "AND " +
            "host.pk_host = ?";

    @Override
    public boolean isPreferShow(HostInterface h) {
        return getJdbcTemplate().queryForObject(IS_PREFER_SHOW,
                Integer.class, h.getHostId()) > 0;
    }

    @Override
    public boolean isNimbyHost(HostInterface h) {
        return getJdbcTemplate().queryForObject(
                "SELECT COUNT(1) FROM host WHERE b_nimby=1 AND pk_host=?",
                Integer.class, h.getHostId()) > 0;
    }

    /**
     * Checks if the passed in name looks like a fully qualified domain name.
     * If so, returns the hostname without the domain. Otherwise returns the passed
     * in name unchanged.
     * @param fqdn - String
     * @return String - hostname
     */
    private String getHostNameFromFQDN(String fqdn, Boolean useLongNames) {
        String hostName;
        Pattern ipPattern = Pattern.compile("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$");
        Matcher ipMatcher = ipPattern.matcher(fqdn);
        if (ipMatcher.matches()){
            hostName = fqdn;
        }
        else if (useLongNames) {
            hostName = fqdn;
            Pattern domainPattern = Pattern.compile(
                ".*(\\.(.*)\\.(co(m|.[a-z]{2})|biz|edu|info|net|org|cn|de|eu|nl))$");
            Matcher domainMatcher = domainPattern.matcher(fqdn);
            if (domainMatcher.matches()){
                hostName = fqdn.replace(domainMatcher.group(1), "");
            }
        }
        else {
            hostName = fqdn.split("\\.")[0];
        }
        return hostName;

    }

    /**
     * Converts the amount of memory reported by the machine
     * to a modificed value which takes into account the
     * operating system and the possibility of user applications.
     *
     * @param host
     * @return
     */
    private long convertMemoryUnits(RenderHost host) {

        long memUnits;
        if (host.getTagsList().contains("64bit")) {
            memUnits = CueUtil.convertKbToFakeKb64bit(host.getTotalMem());
        }
        else {
            memUnits = CueUtil.convertKbToFakeKb32bit(host.getTotalMem());
        }

        /*
         * If this is a desktop, we'll just cut the memory
         * so we don't annoy the user.
         */
        if (host.getNimbyEnabled()) {
            memUnits = (long) (memUnits / 1.5) + Dispatcher.MEM_RESERVED_SYSTEM;
        }

        return memUnits;
    }

}