package leigh.minecraft.link.faction;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import leigh.mecha.cred.Credential;
import leigh.mecha.cred.CredentialConnection;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import java.util.UUID;

/**
 * This class manages faction reputation. Reputation is stored in MySQL in the lc_faction and lc_reputation
 * tables. Reputation is associated with a given player via their UUID, and the tables are largely self
 * explanatory.
 *
 * @author Alex Leigh
 */
public class Reputation {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(Reputation.class);
    private final ComboPooledDataSource cpds;
    private final ReputationEventHandler handler;
    private final ReputationLevels levels = new ReputationLevels();
    private final Faction faction;

    public Reputation(CredentialConnection cred, ReputationEventHandler handler, Faction faction) {
        this.handler = handler;
        this.faction = faction;
        cpds = new ComboPooledDataSource();
        cpds.setJdbcUrl(cred.getConnectionString());
        cpds.setUser(cred.getUsername());
        cpds.setPassword(cred.getPassword());
    }

    public static void main(String[] args) throws SQLException, IOException {
        Properties p = new Properties();
        p.load(new FileInputStream("test.properties"));

        Reputation r = new Reputation(new CredentialConnection("jdbc:mysql://db1.leigh-co.com/neweden",
                new Credential("neweden", p.getProperty("db1_neweden_password"))), new ReputationEventHandler() {
            @Override
            public void reputationChanged(UUID playerId, String factionDisplayName, int delta) {
                logger.info("Reputation changed. [faction: {}] [delta: {}]", factionDisplayName, delta);
            }

            @Override
            public void reputationLevelChanged(UUID playerId, String factionDisplayName, String factionLevel) {
                logger.info("Reputation changed. [faction: {}] [level: {}]", factionDisplayName, factionLevel);
            }
        }, new Faction());

        UUID player = UUID.randomUUID();

        r.adjustReputation(player, Faction.ID_LEIGHCO, 0);
        r.adjustReputation(player, 1, 5000);
        r.adjustReputation(player, 1, 10);
    }

    /**
     * Return the reputation for the given player with the given faction. If there is no faction in the database
     * for this combination, 0 is returned instead.
     * <p>
     * This is an expensive synchronous call.
     */
    public int getReputation(UUID playerId, int factionId) throws SQLException {
        try (Connection con = cpds.getConnection()) {
            try (PreparedStatement selectPs = con.prepareStatement("SELECT (standing) FROM lc_reputation WHERE player_uuid=? AND faction_id=?")) {
                selectPs.setString(1, playerId.toString());
                selectPs.setInt(2, factionId);
                try (ResultSet rs = selectPs.executeQuery()) {
                    if (rs.next()) {
                        return rs.getInt(1);
                    } else {
                        return 0;
                    }
                }
            }
        }
    }

    /**
     * Increase or decrease standing with the given faction for the given player by the given amount. Do not
     * call this method from within a tick, as it will crater TPS or lead to server instability. Call
     * adjustReputationAsync() instead.
     * <p>
     * This method will not perform world-level business logic.
     */
    private void adjustReputationSync(UUID playerId, int factionId, int amount) throws SQLException {
        // We do this to avoid sending messages while in the txn
        String levelChanged = null;

        try (Connection con = cpds.getConnection()) {
            con.setAutoCommit(false);
            try (PreparedStatement selectPs = con.prepareStatement("SELECT (standing) FROM lc_reputation WHERE player_uuid=? AND faction_id=?")) {
                selectPs.setString(1, playerId.toString());
                selectPs.setInt(2, factionId);

                try (ResultSet rs = selectPs.executeQuery()) {
                    if (rs.next()) {
                        int oldAmt = rs.getInt(1);
                        int newAmt = oldAmt + amount;

                        try (PreparedStatement insertPs = con.prepareStatement("UPDATE lc_reputation SET standing=? WHERE player_uuid=? AND faction_id=?")) {
                            insertPs.setInt(1, newAmt);
                            insertPs.setString(2, playerId.toString());
                            insertPs.setInt(3, factionId);
                            insertPs.execute();
                        }

                        String oldLevel = levels.getDisplayText(oldAmt);
                        String newLevel = levels.getDisplayText(newAmt);

                        if (!oldLevel.equals(newLevel)) {
                            levelChanged = newLevel;
                        }
                    } else {
                        try (PreparedStatement updatePs = con.prepareStatement("INSERT INTO lc_reputation (player_uuid,faction_id,standing) VALUES (?,?,?)")) {
                            updatePs.setString(1, playerId.toString());
                            updatePs.setInt(2, factionId);
                            updatePs.setInt(3, amount);
                            updatePs.execute();
                        }

                        // If they had no rep before, but now do, we always announce it.
                        levelChanged = levels.getDisplayText(amount);
                    }
                }
            }

            con.commit();
            String factionName = faction.getDisplayText(factionId);
            handler.reputationChanged(playerId, factionName, amount);
            if (levelChanged != null) {
                handler.reputationLevelChanged(playerId, factionName, levelChanged);
            }
        }
    }

    /**
     * Increase or decrease standing with the given faction for the given player by the given amount. This method
     * is safe to call from within a tick.
     * <p>
     * This method will not perform world-level business logic.
     */
    private void adjustReputationAsync(UUID playerId, int faction, int amount) {
        // TODO Actually use a worker pool here

        new Thread(() -> {
            try {
                adjustReputationSync(playerId, faction, amount);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }).start();
    }

    /**
     * Adjust the reputation for the player, and perform world-specific business rules.
     */
    public void adjustReputation(UUID playerId, int factionId, int amount) {
        adjustReputationAsync(playerId, factionId, amount);

        int x = -amount / 2;

        if (x != 0) {
            switch (factionId) {
                case Faction.ID_LEIGHCO:
                    adjustReputationAsync(playerId, Faction.ID_RETURNER, x);
                    break;
                case Faction.ID_RETURNER:
                    adjustReputationAsync(playerId, Faction.ID_LEIGHCO, x);
                    break;
            }
        }
    }
}
