package server.logic.ws_protocol.JSON.handlers.channels;

import blockchain.BchBlockEntry;
import blockchain.body.BodyRecord;
import blockchain.body.CreateChannelBody;
import blockchain.body.TextBody;
import blockchain.body.TextLineBody;
import blockchain.body.TextReplyBody;
import shine.db.channels.ChannelNameRules;
import shine.db.MsgSubType;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

final class ChannelsReadSupport {
    static final int MSG_TYPE_TEXT = 1;
    static final int MSG_TYPE_REACTION = 2;
    static final int MSG_TYPE_TECH = 0;
    static final String STORIES_CHANNEL_NAME = "stories";
    static final String COMMAND_PREFIX = "/.";
    static final String COMMAND_DESC = "desc";
    static final String COMMAND_ADD = "add";
    static final String COMMAND_REMOVE = "remove";

    private ChannelsReadSupport() {}

    static String canonicalLogin(Connection c, String anyCaseLogin) throws SQLException {
        String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1";
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, anyCaseLogin);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? rs.getString("login") : null;
            }
        }
    }

    static ChannelMeta detectChannelMeta(Connection c, String ownerBch, int rootNumber) throws SQLException {
        ChannelMeta meta = new ChannelMeta();
        meta.channelTypeVersion = CreateChannelBody.CHANNEL_TYPE_VERSION_DEFAULT & 0xFFFF;

        if (rootNumber == 0) {
            meta.channelName = STORIES_CHANNEL_NAME;
            meta.channelDescription = detectLatestDescriptionCommand(c, ownerBch, 0);
            meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_STORIES & 0xFFFF;
            return meta;
        }

        String sql = "SELECT block_bytes FROM blocks WHERE bch_name=? AND block_number=? LIMIT 1";
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, rootNumber);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) {
                    meta.channelName = null;
                    meta.channelDescription = "";
                    meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF;
                    return meta;
                }
                byte[] bytes = rs.getBytes("block_bytes");
                BchBlockEntry e = new BchBlockEntry(bytes);
                BodyRecord body = e.body;
                if (body instanceof CreateChannelBody ccb) {
                    meta.channelName = ccb.channelName;
                    meta.channelDescription = ccb.channelDescription == null ? "" : ccb.channelDescription;
                    meta.channelTypeCode = Short.toUnsignedInt(ccb.channelTypeCode);
                    meta.channelTypeVersion = Short.toUnsignedInt(ccb.channelTypeVersion);
                } else {
                    meta.channelName = null;
                    meta.channelDescription = "";
                    meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF;
                }
                String updatedDescription = detectLatestDescriptionCommand(c, ownerBch, rootNumber);
                if (updatedDescription != null) {
                    meta.channelDescription = updatedDescription;
                }
                return meta;
            } catch (Exception ignored) {
                meta.channelName = null;
                meta.channelDescription = "";
                meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF;
                return meta;
            }
        }
    }

    static int countPosts(Connection c, String ownerBch, int lineCode) throws SQLException {
        String sql = "SELECT COUNT(*) AS cnt FROM blocks WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?) AND line_code=?";
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, MSG_TYPE_TEXT);
            ps.setInt(3, MsgSubType.TEXT_POST);
            ps.setInt(4, MsgSubType.TEXT_REPOST);
            ps.setInt(5, lineCode);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? rs.getInt("cnt") : 0;
            }
        }
    }

    static PostBlock loadLastPost(Connection c, String ownerBch, int lineCode) throws SQLException {
        String sql = """
            SELECT login,bch_name,block_number,block_hash,block_bytes
            FROM blocks
            WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?) AND line_code=?
            ORDER BY block_number DESC
            LIMIT 1
            """;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, MSG_TYPE_TEXT);
            ps.setInt(3, MsgSubType.TEXT_POST);
            ps.setInt(4, MsgSubType.TEXT_REPOST);
            ps.setInt(5, lineCode);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) return null;
                PostBlock pb = new PostBlock();
                pb.login = rs.getString("login");
                pb.bchName = rs.getString("bch_name");
                pb.blockNumber = rs.getInt("block_number");
                pb.blockHash = rs.getBytes("block_hash");
                pb.blockBytes = rs.getBytes("block_bytes");
                return pb;
            }
        }
    }

    static PostBlock loadLastVersion(Connection c, String ownerBch, int originalBlockNumber, byte[] originalHash) throws SQLException {
        String sql = """
            SELECT login,bch_name,block_number,block_hash,block_bytes
            FROM blocks
            WHERE bch_name=? AND msg_type=? AND msg_sub_type=?
              AND to_bch_name=? AND to_block_number=? AND to_block_hash=?
            ORDER BY block_number DESC
            LIMIT 1
            """;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, MSG_TYPE_TEXT);
            ps.setInt(3, MsgSubType.TEXT_EDIT_POST);
            ps.setString(4, ownerBch);
            ps.setInt(5, originalBlockNumber);
            ps.setBytes(6, originalHash);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) return null;
                PostBlock pb = new PostBlock();
                pb.login = rs.getString("login");
                pb.bchName = rs.getString("bch_name");
                pb.blockNumber = rs.getInt("block_number");
                pb.blockHash = rs.getBytes("block_hash");
                pb.blockBytes = rs.getBytes("block_bytes");
                return pb;
            }
        }
    }

    static TextInfo parseTextAndTime(byte[] blockBytes) {
        try {
            BchBlockEntry e = new BchBlockEntry(blockBytes);
            TextInfo ti = new TextInfo();
            ti.createdAtMs = e.timestamp * 1000L;
            if (e.body instanceof TextLineBody tlb) {
                ti.text = tlb.message;
            } else if (e.body instanceof TextReplyBody trb) {
                ti.text = trb.message;
            } else if (e.body instanceof TextBody tb) {
                ti.text = tb.message;
            }
            return ti;
        } catch (Exception ex) {
            return new TextInfo();
        }
    }

    static List<PostBlock> channelPosts(Connection c, String ownerBch, int lineCode, int limit, boolean asc) throws SQLException {
        String order = asc ? "ASC" : "DESC";
        String sql = """
            SELECT login,bch_name,block_number,block_hash,block_bytes,to_bch_name,to_block_number,to_block_hash,msg_sub_type,this_line_number
            FROM blocks
            WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?) AND line_code=?
            ORDER BY block_number
            """ + order + " LIMIT ?";
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, MSG_TYPE_TEXT);
            ps.setInt(3, MsgSubType.TEXT_POST);
            ps.setInt(4, MsgSubType.TEXT_REPOST);
            ps.setInt(5, lineCode);
            ps.setInt(6, limit);
            try (ResultSet rs = ps.executeQuery()) {
                List<PostBlock> out = new ArrayList<>();
                while (rs.next()) {
                    PostBlock pb = new PostBlock();
                    pb.login = rs.getString("login");
                    pb.bchName = rs.getString("bch_name");
                    pb.blockNumber = rs.getInt("block_number");
                    pb.blockHash = rs.getBytes("block_hash");
                    pb.blockBytes = rs.getBytes("block_bytes");
                    pb.toBchName = rs.getString("to_bch_name");
                    pb.toBlockNumber = (Integer) rs.getObject("to_block_number");
                    pb.toBlockHash = rs.getBytes("to_block_hash");
                    pb.msgSubType = rs.getInt("msg_sub_type");
                    pb.thisLineNumber = (Integer) rs.getObject("this_line_number");
                    out.add(pb);
                }
                return out;
            }
        }
    }

    static List<PostBlock> mergeSortedByTime(List<PostBlock> source) {
        List<PostBlock> out = new ArrayList<>(source);
        out.sort(Comparator
                .comparingLong((PostBlock pb) -> parseTextAndTime(pb.blockBytes).createdAtMs)
                .thenComparing(pb -> String.valueOf(pb.bchName))
                .thenComparingInt(pb -> pb.blockNumber));
        return out;
    }

    static List<PostBlock> versionsForPost(Connection c, String ownerBch, int originalBlock, byte[] originalHash) throws SQLException {
        String sql = """
            SELECT login,bch_name,block_number,block_hash,block_bytes
            FROM blocks
            WHERE bch_name=? AND msg_type=? AND msg_sub_type=?
              AND to_block_number=? AND to_block_hash=?
              AND (to_bch_name=? OR to_bch_name IS NULL OR to_bch_name='')
            ORDER BY block_number ASC
            """;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, MSG_TYPE_TEXT);
            ps.setInt(3, MsgSubType.TEXT_EDIT_POST);
            ps.setInt(4, originalBlock);
            ps.setBytes(5, originalHash);
            ps.setString(6, ownerBch);
            try (ResultSet rs = ps.executeQuery()) {
                List<PostBlock> out = new ArrayList<>();
                while (rs.next()) {
                    PostBlock pb = new PostBlock();
                    pb.login = rs.getString("login");
                    pb.bchName = rs.getString("bch_name");
                    pb.blockNumber = rs.getInt("block_number");
                    pb.blockHash = rs.getBytes("block_hash");
                    pb.blockBytes = rs.getBytes("block_bytes");
                    out.add(pb);
                }
                return out;
            }
        }
    }

    static int[] loadStats(Connection c, String bch, int blockNumber, byte[] blockHash) throws SQLException {
        String sql = "SELECT likes_count,replies_count FROM message_stats WHERE to_bch_name=? AND to_block_number=? AND to_block_hash=? LIMIT 1";
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, bch);
            ps.setInt(2, blockNumber);
            ps.setBytes(3, blockHash);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) return new int[] {0, 0};
                return new int[] {rs.getInt("likes_count"), rs.getInt("replies_count")};
            }
        }
    }

    static String detectChannelDescription(Connection c, String ownerBch, int rootNumber) throws SQLException {
        if (rootNumber == 0) {
            String fromCommand = detectLatestDescriptionCommand(c, ownerBch, 0);
            return fromCommand == null ? "" : fromCommand;
        }

        // Preferred source: persisted state (fast path, works for CreateChannelBody v2).
        String stateSql = """
            SELECT channel_description
            FROM channel_names_state
            WHERE owner_bch_name = ? AND channel_root_block_number = ?
            LIMIT 1
            """;
        try (PreparedStatement ps = c.prepareStatement(stateSql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, rootNumber);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    String saved = String.valueOf(rs.getString("channel_description") == null ? "" : rs.getString("channel_description"));
                    String fromCommand = detectLatestDescriptionCommand(c, ownerBch, rootNumber);
                    return fromCommand == null ? saved : fromCommand;
                }
            }
        } catch (SQLException ignored) {
            // keep compatibility for environments where table schema is older/corrupted
        }

        // Fallback: parse root block directly.
        String sql = "SELECT block_bytes FROM blocks WHERE bch_name=? AND block_number=? LIMIT 1";
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, rootNumber);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) return "";
                byte[] bytes = rs.getBytes("block_bytes");
                BchBlockEntry e = new BchBlockEntry(bytes);
                BodyRecord body = e.body;
                if (body instanceof CreateChannelBody ccb) {
                    String fromCommand = detectLatestDescriptionCommand(c, ownerBch, rootNumber);
                    if (fromCommand != null) return fromCommand;
                    return ccb.channelDescription == null ? "" : ccb.channelDescription;
                }
                return "";
            } catch (Exception ignored) {
                return "";
            }
        }
    }

    static Integer detectChannelTypeCode(Connection c, String ownerBch, int rootNumber) throws SQLException {
        if (rootNumber == 0) return CreateChannelBody.CHANNEL_TYPE_STORIES & 0xFFFF;

        String stateSql = """
            SELECT channel_type_code
            FROM channel_names_state
            WHERE owner_bch_name = ? AND channel_root_block_number = ?
            LIMIT 1
            """;
        try (PreparedStatement ps = c.prepareStatement(stateSql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, rootNumber);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) return rs.getInt("channel_type_code");
            }
        } catch (SQLException ignored) {
            // fallback below
        }

        ChannelMeta meta = detectChannelMeta(c, ownerBch, rootNumber);
        return meta.channelTypeCode;
    }

    static Integer detectChannelTypeVersion(Connection c, String ownerBch, int rootNumber) throws SQLException {
        if (rootNumber == 0) return CreateChannelBody.CHANNEL_TYPE_VERSION_DEFAULT & 0xFFFF;

        String stateSql = """
            SELECT channel_type_version
            FROM channel_names_state
            WHERE owner_bch_name = ? AND channel_root_block_number = ?
            LIMIT 1
            """;
        try (PreparedStatement ps = c.prepareStatement(stateSql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, rootNumber);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) return rs.getInt("channel_type_version");
            }
        } catch (SQLException ignored) {
            // fallback below
        }

        ChannelMeta meta = detectChannelMeta(c, ownerBch, rootNumber);
        return meta.channelTypeVersion;
    }

    static String detectLatestDescriptionCommand(Connection c, String ownerBch, int lineCode) throws SQLException {
        String sql = """
            SELECT block_bytes
            FROM blocks
            WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?) AND line_code=?
            ORDER BY block_number DESC
            LIMIT 300
            """;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, ownerBch);
            ps.setInt(2, MSG_TYPE_TEXT);
            ps.setInt(3, MsgSubType.TEXT_POST);
            ps.setInt(4, MsgSubType.TEXT_REPOST);
            ps.setInt(5, lineCode);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    TextInfo info = parseTextAndTime(rs.getBytes("block_bytes"));
                    CommandInfo commandInfo = parseCommandText(info.text);
                    if (commandInfo != null && COMMAND_DESC.equals(commandInfo.command)) {
                        return commandInfo.arg;
                    }
                }
            }
        }
        return null;
    }

    static CommandInfo parseCommandText(String text) {
        String value = String.valueOf(text == null ? "" : text).trim();
        if (!value.startsWith(COMMAND_PREFIX)) return null;

        String raw = value.substring(COMMAND_PREFIX.length()).trim();
        if (raw.isEmpty()) return null;

        int sp = raw.indexOf(' ');
        String cmd = (sp < 0 ? raw : raw.substring(0, sp)).trim().toLowerCase();
        String arg = sp < 0 ? "" : raw.substring(sp + 1).trim();
        if (cmd.isEmpty()) return null;
        return new CommandInfo(cmd, arg);
    }

    static PairChannelSelector findPersonalPairChannel(Connection c, String ownerLogin, String partnerLogin) throws SQLException {
        if (ownerLogin == null || ownerLogin.isBlank() || partnerLogin == null || partnerLogin.isBlank()) return null;

        String canonicalPartner = canonicalLogin(c, partnerLogin);
        if (canonicalPartner == null || canonicalPartner.isBlank()) return null;

        String partnerBchSql = """
            SELECT blockchain_name
            FROM blockchain_state
            WHERE login = ? COLLATE NOCASE
            ORDER BY blockchain_name
            LIMIT 1
            """;

        String partnerBch = null;
        try (PreparedStatement ps = c.prepareStatement(partnerBchSql)) {
            ps.setString(1, canonicalPartner);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) partnerBch = rs.getString("blockchain_name");
            }
        }
        if (partnerBch == null || partnerBch.isBlank()) return null;

        String ownerSlug;
        try {
            ownerSlug = ChannelNameRules.toCanonicalSlug(ownerLogin);
        } catch (Exception e) {
            ownerSlug = ownerLogin.trim().toLowerCase();
        }

        String rootSql = """
            SELECT channel_root_block_number
            FROM channel_names_state
            WHERE owner_bch_name = ?
              AND channel_type_code = ?
              AND slug = ?
            ORDER BY channel_root_block_number DESC
            LIMIT 1
            """;
        try (PreparedStatement ps = c.prepareStatement(rootSql)) {
            ps.setString(1, partnerBch);
            ps.setInt(2, CreateChannelBody.CHANNEL_TYPE_PERSONAL & 0xFFFF);
            ps.setString(3, ownerSlug);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) return null;
                PairChannelSelector out = new PairChannelSelector();
                out.ownerBlockchainName = partnerBch;
                out.channelRootBlockNumber = rs.getInt("channel_root_block_number");
                return out;
            }
        }
    }

    static boolean isLikedByLogin(Connection c, String login, String toBch, int toBlockNumber, byte[] toBlockHash) throws SQLException {
        if (login == null || login.isBlank() || toBch == null || toBch.isBlank() || toBlockHash == null || toBlockHash.length != 32) {
            return false;
        }
        String sql = """
            SELECT msg_sub_type
            FROM blocks
            WHERE login = ? COLLATE NOCASE
              AND msg_type = ?
              AND to_bch_name = ?
              AND to_block_number = ?
              AND to_block_hash = ?
            ORDER BY block_number DESC
            LIMIT 1
            """;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, login);
            ps.setInt(2, MSG_TYPE_REACTION);
            ps.setString(3, toBch);
            ps.setInt(4, toBlockNumber);
            ps.setBytes(5, toBlockHash);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) return false;
                return rs.getInt("msg_sub_type") == MsgSubType.REACTION_LIKE;
            }
        }
    }

    static byte[] hexToBytes(String s) {
        if (s == null) return null;
        String x = s.trim();
        if ((x.length() & 1) != 0) throw new IllegalArgumentException("hex length must be even");
        byte[] out = new byte[x.length() / 2];
        for (int i = 0; i < out.length; i++) {
            int hi = Character.digit(x.charAt(i * 2), 16);
            int lo = Character.digit(x.charAt(i * 2 + 1), 16);
            if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex");
            out[i] = (byte) ((hi << 4) | lo);
        }
        return out;
    }

    static String toHex(byte[] bytes) {
        if (bytes == null) return null;
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) sb.append(String.format("%02x", b));
        return sb.toString();
    }

    static final class PostBlock {
        String login;
        String bchName;
        int blockNumber;
        byte[] blockHash;
        byte[] blockBytes;
        String toBchName;
        Integer toBlockNumber;
        byte[] toBlockHash;
        int msgSubType;
        Integer thisLineNumber;
    }

    static final class TextInfo {
        String text = "";
        long createdAtMs = 0L;
    }

    static final class ChannelMeta {
        String channelName;
        String channelDescription;
        int channelTypeCode;
        int channelTypeVersion;
    }

    static final class CommandInfo {
        final String command;
        final String arg;

        CommandInfo(String command, String arg) {
            this.command = command;
            this.arg = arg;
        }
    }

    static final class PairChannelSelector {
        String ownerBlockchainName;
        int channelRootBlockNumber;
    }
}
