package io.synor.ibc;

import java.math.BigInteger;
import java.util.List;
import java.util.Map;

/**
 * Synor IBC SDK Types for Java
 *
 * Inter-Blockchain Communication (IBC) protocol types.
 */
public class Types {

    /**
     * IBC Height representation
     */
    public static class Height {
        private final long revisionNumber;
        private final long revisionHeight;

        public Height() {
            this(0, 1);
        }

        public Height(long revisionNumber, long revisionHeight) {
            this.revisionNumber = revisionNumber;
            this.revisionHeight = revisionHeight;
        }

        public long getRevisionNumber() { return revisionNumber; }
        public long getRevisionHeight() { return revisionHeight; }

        public boolean isZero() {
            return revisionNumber == 0 && revisionHeight == 0;
        }

        public Height increment() {
            return new Height(revisionNumber, revisionHeight + 1);
        }

        public static Height fromJson(Map<String, Object> json) {
            return new Height(
                ((Number) json.getOrDefault("revision_number", 0)).longValue(),
                ((Number) json.getOrDefault("revision_height", 1)).longValue()
            );
        }

        public Map<String, Object> toJson() {
            return Map.of(
                "revision_number", revisionNumber,
                "revision_height", revisionHeight
            );
        }
    }

    /**
     * Chain identifier
     */
    public static class ChainId {
        private final String id;

        public ChainId(String id) {
            this.id = id;
        }

        public String getId() { return id; }

        public static ChainId fromJson(Map<String, Object> json) {
            return new ChainId((String) json.get("id"));
        }

        public Map<String, Object> toJson() {
            return Map.of("id", id);
        }
    }

    /**
     * IBC Version with features
     */
    public static class Version {
        private final String identifier;
        private final List<String> features;

        public Version(String identifier, List<String> features) {
            this.identifier = identifier;
            this.features = features;
        }

        public String getIdentifier() { return identifier; }
        public List<String> getFeatures() { return features; }

        public static Version defaultConnection() {
            return new Version("1", List.of("ORDER_ORDERED", "ORDER_UNORDERED"));
        }

        @SuppressWarnings("unchecked")
        public static Version fromJson(Map<String, Object> json) {
            return new Version(
                (String) json.get("identifier"),
                (List<String>) json.get("features")
            );
        }

        public Map<String, Object> toJson() {
            return Map.of(
                "identifier", identifier,
                "features", features
            );
        }
    }

    /**
     * Light client types
     */
    public enum ClientType {
        TENDERMINT("tendermint"),
        SOLO_MACHINE("solo_machine"),
        LOCALHOST("localhost"),
        WASM("wasm");

        private final String value;

        ClientType(String value) {
            this.value = value;
        }

        public String getValue() { return value; }
    }

    /**
     * Light client identifier
     */
    public static class ClientId {
        private final String id;

        public ClientId(String id) {
            this.id = id;
        }

        public String getId() { return id; }

        public static ClientId fromJson(Map<String, Object> json) {
            return new ClientId((String) json.get("id"));
        }

        public Map<String, Object> toJson() {
            return Map.of("id", id);
        }
    }

    /**
     * Trust level configuration
     */
    public static class TrustLevel {
        private final int numerator;
        private final int denominator;

        public TrustLevel() {
            this(1, 3);
        }

        public TrustLevel(int numerator, int denominator) {
            this.numerator = numerator;
            this.denominator = denominator;
        }

        public int getNumerator() { return numerator; }
        public int getDenominator() { return denominator; }

        public static TrustLevel fromJson(Map<String, Object> json) {
            return new TrustLevel(
                ((Number) json.getOrDefault("numerator", 1)).intValue(),
                ((Number) json.getOrDefault("denominator", 3)).intValue()
            );
        }

        public Map<String, Object> toJson() {
            return Map.of(
                "numerator", numerator,
                "denominator", denominator
            );
        }
    }

    /**
     * Light client state
     */
    public static class ClientState {
        private final String chainId;
        private final TrustLevel trustLevel;
        private final long trustingPeriod;
        private final long unbondingPeriod;
        private final long maxClockDrift;
        private final Height latestHeight;
        private final Height frozenHeight;

        public ClientState(String chainId, TrustLevel trustLevel, long trustingPeriod,
                          long unbondingPeriod, long maxClockDrift, Height latestHeight,
                          Height frozenHeight) {
            this.chainId = chainId;
            this.trustLevel = trustLevel;
            this.trustingPeriod = trustingPeriod;
            this.unbondingPeriod = unbondingPeriod;
            this.maxClockDrift = maxClockDrift;
            this.latestHeight = latestHeight;
            this.frozenHeight = frozenHeight;
        }

        public String getChainId() { return chainId; }
        public TrustLevel getTrustLevel() { return trustLevel; }
        public long getTrustingPeriod() { return trustingPeriod; }
        public long getUnbondingPeriod() { return unbondingPeriod; }
        public long getMaxClockDrift() { return maxClockDrift; }
        public Height getLatestHeight() { return latestHeight; }
        public Height getFrozenHeight() { return frozenHeight; }

        @SuppressWarnings("unchecked")
        public static ClientState fromJson(Map<String, Object> json) {
            return new ClientState(
                (String) json.get("chain_id"),
                TrustLevel.fromJson((Map<String, Object>) json.get("trust_level")),
                ((Number) json.get("trusting_period")).longValue(),
                ((Number) json.get("unbonding_period")).longValue(),
                ((Number) json.get("max_clock_drift")).longValue(),
                Height.fromJson((Map<String, Object>) json.get("latest_height")),
                json.get("frozen_height") != null
                    ? Height.fromJson((Map<String, Object>) json.get("frozen_height"))
                    : null
            );
        }

        public Map<String, Object> toJson() {
            var map = new java.util.HashMap<String, Object>();
            map.put("chain_id", chainId);
            map.put("trust_level", trustLevel.toJson());
            map.put("trusting_period", trustingPeriod);
            map.put("unbonding_period", unbondingPeriod);
            map.put("max_clock_drift", maxClockDrift);
            map.put("latest_height", latestHeight.toJson());
            if (frozenHeight != null) {
                map.put("frozen_height", frozenHeight.toJson());
            }
            return map;
        }
    }

    /**
     * Connection state
     */
    public enum ConnectionState {
        UNINITIALIZED, INIT, TRYOPEN, OPEN
    }

    /**
     * Connection identifier
     */
    public static class ConnectionId {
        private final String id;

        public ConnectionId(String id) {
            this.id = id;
        }

        public String getId() { return id; }

        public static ConnectionId newId(int sequence) {
            return new ConnectionId("connection-" + sequence);
        }

        public static ConnectionId fromJson(Map<String, Object> json) {
            return new ConnectionId((String) json.get("id"));
        }

        public Map<String, Object> toJson() {
            return Map.of("id", id);
        }
    }

    /**
     * Port identifier
     */
    public static class PortId {
        private final String id;

        public PortId(String id) {
            this.id = id;
        }

        public String getId() { return id; }

        public static PortId transfer() {
            return new PortId("transfer");
        }

        public static PortId fromJson(Map<String, Object> json) {
            return new PortId((String) json.get("id"));
        }

        public Map<String, Object> toJson() {
            return Map.of("id", id);
        }
    }

    /**
     * Channel identifier
     */
    public static class ChannelId {
        private final String id;

        public ChannelId(String id) {
            this.id = id;
        }

        public String getId() { return id; }

        public static ChannelId newId(int sequence) {
            return new ChannelId("channel-" + sequence);
        }

        public static ChannelId fromJson(Map<String, Object> json) {
            return new ChannelId((String) json.get("id"));
        }

        public Map<String, Object> toJson() {
            return Map.of("id", id);
        }
    }

    /**
     * Channel ordering
     */
    public enum ChannelOrder {
        UNORDERED, ORDERED
    }

    /**
     * Channel state
     */
    public enum ChannelState {
        UNINITIALIZED, INIT, TRYOPEN, OPEN, CLOSED
    }

    /**
     * Timeout information
     */
    public static class Timeout {
        private final Height height;
        private final BigInteger timestamp;

        public Timeout(Height height) {
            this(height, BigInteger.ZERO);
        }

        public Timeout(Height height, BigInteger timestamp) {
            this.height = height;
            this.timestamp = timestamp;
        }

        public Height getHeight() { return height; }
        public BigInteger getTimestamp() { return timestamp; }

        public static Timeout fromHeight(long height) {
            return new Timeout(new Height(0, height));
        }

        public static Timeout fromTimestamp(BigInteger timestamp) {
            return new Timeout(new Height(), timestamp);
        }

        public Map<String, Object> toJson() {
            return Map.of(
                "height", height.toJson(),
                "timestamp", timestamp.toString()
            );
        }
    }

    /**
     * Transfer packet data (ICS-20)
     */
    public static class FungibleTokenPacketData {
        private final String denom;
        private final String amount;
        private final String sender;
        private final String receiver;
        private final String memo;

        public FungibleTokenPacketData(String denom, String amount, String sender,
                                       String receiver, String memo) {
            this.denom = denom;
            this.amount = amount;
            this.sender = sender;
            this.receiver = receiver;
            this.memo = memo != null ? memo : "";
        }

        public String getDenom() { return denom; }
        public String getAmount() { return amount; }
        public String getSender() { return sender; }
        public String getReceiver() { return receiver; }
        public String getMemo() { return memo; }

        public boolean isNative() {
            return !denom.contains("/");
        }

        public static FungibleTokenPacketData fromJson(Map<String, Object> json) {
            return new FungibleTokenPacketData(
                (String) json.get("denom"),
                (String) json.get("amount"),
                (String) json.get("sender"),
                (String) json.get("receiver"),
                (String) json.getOrDefault("memo", "")
            );
        }

        public Map<String, Object> toJson() {
            return Map.of(
                "denom", denom,
                "amount", amount,
                "sender", sender,
                "receiver", receiver,
                "memo", memo
            );
        }
    }

    /**
     * Swap state
     */
    public enum SwapState {
        PENDING, LOCKED, COMPLETED, REFUNDED, EXPIRED, CANCELLED
    }

    /**
     * Swap identifier
     */
    public static class SwapId {
        private final String id;

        public SwapId(String id) {
            this.id = id;
        }

        public String getId() { return id; }

        public static SwapId fromJson(Map<String, Object> json) {
            return new SwapId((String) json.get("id"));
        }

        public Map<String, Object> toJson() {
            return Map.of("id", id);
        }
    }

    /**
     * Hashlock - hash of the secret
     */
    public static class Hashlock {
        private final byte[] hash;

        public Hashlock(byte[] hash) {
            this.hash = hash;
        }

        public byte[] getHash() { return hash; }

        @SuppressWarnings("unchecked")
        public static Hashlock fromJson(Map<String, Object> json) {
            List<Number> hashList = (List<Number>) json.get("hash");
            byte[] hash = new byte[hashList.size()];
            for (int i = 0; i < hashList.size(); i++) {
                hash[i] = hashList.get(i).byteValue();
            }
            return new Hashlock(hash);
        }

        public Map<String, Object> toJson() {
            int[] hashArray = new int[hash.length];
            for (int i = 0; i < hash.length; i++) {
                hashArray[i] = hash[i] & 0xFF;
            }
            return Map.of("hash", hashArray);
        }
    }

    /**
     * Timelock - expiration time
     */
    public static class Timelock {
        private final BigInteger expiry;

        public Timelock(BigInteger expiry) {
            this.expiry = expiry;
        }

        public BigInteger getExpiry() { return expiry; }

        public boolean isExpired(BigInteger current) {
            return current.compareTo(expiry) >= 0;
        }

        public static Timelock fromJson(Map<String, Object> json) {
            return new Timelock(new BigInteger(json.get("expiry").toString()));
        }

        public Map<String, Object> toJson() {
            return Map.of("expiry", expiry.toString());
        }
    }

    /**
     * Atomic swap
     */
    public static class AtomicSwap {
        private final SwapId swapId;
        private final SwapState state;
        private final Map<String, Object> initiatorHtlc;
        private final Map<String, Object> responderHtlc;

        public AtomicSwap(SwapId swapId, SwapState state,
                         Map<String, Object> initiatorHtlc,
                         Map<String, Object> responderHtlc) {
            this.swapId = swapId;
            this.state = state;
            this.initiatorHtlc = initiatorHtlc;
            this.responderHtlc = responderHtlc;
        }

        public SwapId getSwapId() { return swapId; }
        public SwapState getState() { return state; }
        public Map<String, Object> getInitiatorHtlc() { return initiatorHtlc; }
        public Map<String, Object> getResponderHtlc() { return responderHtlc; }

        @SuppressWarnings("unchecked")
        public static AtomicSwap fromJson(Map<String, Object> json) {
            return new AtomicSwap(
                SwapId.fromJson((Map<String, Object>) json.get("swap_id")),
                SwapState.valueOf(((String) json.get("state")).toUpperCase()),
                (Map<String, Object>) json.get("initiator_htlc"),
                (Map<String, Object>) json.get("responder_htlc")
            );
        }
    }

    /**
     * IBC SDK configuration
     */
    public static class IbcConfig {
        private final String apiKey;
        private final String endpoint;
        private final String wsEndpoint;
        private final int timeout;
        private final int retries;
        private final String chainId;
        private final boolean debug;

        public IbcConfig(String apiKey) {
            this(apiKey, "https://ibc.synor.io/v1", "wss://ibc.synor.io/v1/ws",
                 30, 3, "synor-1", false);
        }

        public IbcConfig(String apiKey, String endpoint, String wsEndpoint,
                        int timeout, int retries, String chainId, boolean debug) {
            this.apiKey = apiKey;
            this.endpoint = endpoint;
            this.wsEndpoint = wsEndpoint;
            this.timeout = timeout;
            this.retries = retries;
            this.chainId = chainId;
            this.debug = debug;
        }

        public String getApiKey() { return apiKey; }
        public String getEndpoint() { return endpoint; }
        public String getWsEndpoint() { return wsEndpoint; }
        public int getTimeout() { return timeout; }
        public int getRetries() { return retries; }
        public String getChainId() { return chainId; }
        public boolean isDebug() { return debug; }

        public static Builder builder(String apiKey) {
            return new Builder(apiKey);
        }

        public static class Builder {
            private String apiKey;
            private String endpoint = "https://ibc.synor.io/v1";
            private String wsEndpoint = "wss://ibc.synor.io/v1/ws";
            private int timeout = 30;
            private int retries = 3;
            private String chainId = "synor-1";
            private boolean debug = false;

            public Builder(String apiKey) {
                this.apiKey = apiKey;
            }

            public Builder endpoint(String endpoint) { this.endpoint = endpoint; return this; }
            public Builder wsEndpoint(String wsEndpoint) { this.wsEndpoint = wsEndpoint; return this; }
            public Builder timeout(int timeout) { this.timeout = timeout; return this; }
            public Builder retries(int retries) { this.retries = retries; return this; }
            public Builder chainId(String chainId) { this.chainId = chainId; return this; }
            public Builder debug(boolean debug) { this.debug = debug; return this; }

            public IbcConfig build() {
                return new IbcConfig(apiKey, endpoint, wsEndpoint, timeout, retries, chainId, debug);
            }
        }
    }

    /**
     * IBC exception
     */
    public static class IbcException extends RuntimeException {
        private final String code;
        private final Integer status;

        public IbcException(String message) {
            this(message, null, null);
        }

        public IbcException(String message, String code, Integer status) {
            super(message);
            this.code = code;
            this.status = status;
        }

        public String getCode() { return code; }
        public Integer getStatus() { return status; }

        @Override
        public String toString() {
            return "IbcException: " + getMessage() + (code != null ? " (" + code + ")" : "");
        }
    }
}
