package com.dexorder.flink.iceberg;

import org.apache.iceberg.Schema;
import org.apache.iceberg.Table;
import org.apache.iceberg.catalog.Catalog;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.SupportsNamespaces;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.types.Types;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;

import java.io.IOException;
import java.net.URI;

import static org.apache.iceberg.types.Types.NestedField.optional;
import static org.apache.iceberg.types.Types.NestedField.required;

/**
 * Initializes Iceberg tables directly via the Iceberg Catalog API.
 *
 * Creates tables in Iceberg if they don't already exist. This ensures all tables
 * are properly initialized before Flink starts writing data.
 */
public class SchemaInitializer {
    private static final Logger LOG = LoggerFactory.getLogger(SchemaInitializer.class);

    private final Catalog catalog;
    private final String namespace;
    private final String s3Endpoint;
    private final String warehouseBucket;

    public SchemaInitializer(Catalog catalog, String namespace, String s3Endpoint, String warehouseBucket) {
        this.catalog = catalog;
        this.namespace = namespace;
        this.s3Endpoint = s3Endpoint;
        this.warehouseBucket = warehouseBucket;
    }

    /**
     * Initialize all schemas by creating tables from definitions.
     *
     * @throws IOException if initialization fails
     */
    public void initializeSchemas() throws IOException {
        LOG.info("Initializing Iceberg schemas in namespace: {}", namespace);

        // Ensure S3 bucket exists before attempting to create tables
        ensureS3BucketExists();

        // Ensure namespace exists
        ensureNamespaceExists();

        // Initialize each table
        initializeOhlcTable();
        initializeSymbolMetadataTable();

        // Add more table initializations here as needed

        LOG.info("Schema initialization completed successfully");
    }

    /**
     * Ensure the S3 warehouse bucket exists, creating it if necessary.
     * Runs before any table creation so a fresh MinIO deployment doesn't crash Flink.
     */
    private void ensureS3BucketExists() {
        if (s3Endpoint == null || warehouseBucket == null || warehouseBucket.isEmpty()) {
            LOG.warn("S3 endpoint or warehouse bucket not configured, skipping bucket check");
            return;
        }
        LOG.info("Ensuring S3 bucket '{}' exists at {}", warehouseBucket, s3Endpoint);
        try (S3Client s3 = S3Client.builder()
                .endpointOverride(URI.create(s3Endpoint))
                .region(Region.of("us-east-1"))
                .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build())
                .credentialsProvider(DefaultCredentialsProvider.create())
                .build()) {
            try {
                s3.headBucket(HeadBucketRequest.builder().bucket(warehouseBucket).build());
                LOG.info("S3 bucket '{}' already exists", warehouseBucket);
            } catch (NoSuchBucketException e) {
                LOG.warn("S3 bucket '{}' not found — creating it now", warehouseBucket);
                s3.createBucket(CreateBucketRequest.builder().bucket(warehouseBucket).build());
                LOG.info("Created S3 bucket '{}'", warehouseBucket);
            }
        } catch (Exception e) {
            LOG.error("Failed to ensure S3 bucket '{}' exists at {}", warehouseBucket, s3Endpoint, e);
            throw new RuntimeException("S3 bucket initialization failed for: " + warehouseBucket, e);
        }
    }

    /**
     * Ensure the namespace exists in the catalog.
     */
    private void ensureNamespaceExists() {
        Namespace ns = Namespace.of(namespace);
        try {
            if (catalog instanceof SupportsNamespaces) {
                SupportsNamespaces nsCatalog = (SupportsNamespaces) catalog;
                if (!nsCatalog.namespaceExists(ns)) {
                    nsCatalog.createNamespace(ns);
                    LOG.info("Created namespace: {}", namespace);
                } else {
                    LOG.info("Namespace already exists: {}", namespace);
                }
            } else {
                LOG.warn("Catalog does not support namespaces, skipping namespace creation");
            }
        } catch (Exception e) {
            LOG.error("Failed to create namespace: {}", namespace, e);
            throw new RuntimeException("Namespace creation failed", e);
        }
    }

    /**
     * Initialize the OHLC table if it doesn't exist.
     */
    // Bump this when the schema changes. Tables with a different (or missing) version
    // will be dropped and recreated. Increment by 1 for each incompatible change.
    // v1: open/high/low/close required; ingestor forward-fills interior gaps with previous close
    private static final String OHLC_SCHEMA_VERSION = "1";
    private static final String SCHEMA_VERSION_PROP = "app.schema.version";

    private void initializeOhlcTable() {
        TableIdentifier tableId = TableIdentifier.of(namespace, "ohlc");

        try {
            boolean tableExists = false;
            try {
                tableExists = catalog.tableExists(tableId);
            } catch (org.apache.iceberg.exceptions.ServiceFailureException e) {
                // Handle corrupted table metadata (e.g., missing S3 files)
                if (e.getMessage().contains("Location does not exist")) {
                    LOG.warn("Table {} has corrupted metadata, dropping and recreating", tableId);
                    try {
                        catalog.dropTable(tableId, false);
                    } catch (Exception dropEx) {
                        LOG.warn("Failed to drop corrupted table (may not exist in catalog)", dropEx);
                    }
                    tableExists = false;
                } else {
                    throw e;
                }
            }

            if (tableExists) {
                Table existing = catalog.loadTable(tableId);
                String existingVersion = existing.properties().get(SCHEMA_VERSION_PROP);
                if (!OHLC_SCHEMA_VERSION.equals(existingVersion)) {
                    LOG.warn("Table {} has schema version '{}', expected '{}' — skipping (manual migration required if needed)",
                            tableId, existingVersion, OHLC_SCHEMA_VERSION);
                }
                LOG.info("Table {} already exists at schema version {} — skipping creation", tableId, existingVersion);
                return;
            }

            LOG.info("Creating OHLC table: {}", tableId);

            // Define the OHLC schema.
            // timestamp is stored as BIGINT (nanoseconds since epoch), not a TIMESTAMP type,
            // so that GenericRowData.setField() accepts a plain Long value.
            Schema schema = new Schema(
                // Primary key fields
                required(1, "ticker", Types.StringType.get(), "Market identifier in Nautilus format (e.g., BTC/USDT.BINANCE)"),
                required(2, "period_seconds", Types.IntegerType.get(), "OHLC period in seconds"),
                required(3, "timestamp", Types.LongType.get(), "Candle timestamp in nanoseconds since epoch"),

                // OHLC price data — required; ingestor forward-fills interior gaps with previous close
                required(4, "open",  Types.LongType.get(), "Opening price (forward-filled for interior market gaps)"),
                required(5, "high",  Types.LongType.get(), "Highest price"),
                required(6, "low",   Types.LongType.get(), "Lowest price"),
                required(7, "close", Types.LongType.get(), "Closing price"),

                // Volume data
                optional(8, "volume", Types.LongType.get(), "Total volume"),
                optional(9, "buy_vol", Types.LongType.get(), "Buy volume"),
                optional(10, "sell_vol", Types.LongType.get(), "Sell volume"),

                // Timing data
                optional(11, "open_time", Types.LongType.get(), "Timestamp when open price occurred"),
                optional(12, "high_time", Types.LongType.get(), "Timestamp when high price occurred"),
                optional(13, "low_time", Types.LongType.get(), "Timestamp when low price occurred"),
                optional(14, "close_time", Types.LongType.get(), "Timestamp when close price occurred"),

                // Additional fields
                optional(15, "open_interest", Types.LongType.get(), "Open interest for futures"),

                // Metadata fields
                optional(16, "request_id", Types.StringType.get(), "Request ID that generated this data"),
                required(17, "ingested_at", Types.LongType.get(), "Timestamp when data was ingested by Flink (nanoseconds since epoch)")
            );

            // Create the table with partitioning and properties
            Table table = catalog.buildTable(tableId, schema)
                    .withPartitionSpec(org.apache.iceberg.PartitionSpec.builderFor(schema)
                            .identity("ticker")
                            .build())
                    .withProperty("write.format.default", "parquet")
                    .withProperty("write.parquet.compression-codec", "snappy")
                    .withProperty("write.metadata.compression-codec", "gzip")
                    .withProperty("format-version", "2")
                    .withProperty(SCHEMA_VERSION_PROP, OHLC_SCHEMA_VERSION)
                    .create();

            LOG.info("Successfully created OHLC table: {}", tableId);

        } catch (Exception e) {
            LOG.error("Failed to initialize OHLC table: {}", tableId, e);
            throw new RuntimeException("OHLC table initialization failed", e);
        }
    }

    /**
     * Initialize the symbol_metadata table if it doesn't exist.
     */
    // v2: removed tick_denom/base_denom/quote_denom; added Nautilus instrument fields
    //     (price_precision, size_precision, tick_size, lot_size, min_notional,
    //      margin_init, margin_maint, maker_fee, taker_fee, contract_multiplier)
    private static final String SYMBOL_METADATA_SCHEMA_VERSION = "1";

    private void initializeSymbolMetadataTable() {
        TableIdentifier tableId = TableIdentifier.of(namespace, "symbol_metadata");

        try {
            boolean tableExists = false;
            try {
                tableExists = catalog.tableExists(tableId);
            } catch (org.apache.iceberg.exceptions.ServiceFailureException e) {
                // Handle corrupted table metadata (e.g., missing S3 files)
                if (e.getMessage().contains("Location does not exist")) {
                    LOG.warn("Table {} has corrupted metadata, dropping and recreating", tableId);
                    try {
                        catalog.dropTable(tableId, false);
                    } catch (Exception dropEx) {
                        LOG.warn("Failed to drop corrupted table (may not exist in catalog)", dropEx);
                    }
                    tableExists = false;
                } else {
                    throw e;
                }
            }

            if (tableExists) {
                Table existing = catalog.loadTable(tableId);
                String existingVersion = existing.properties().get(SCHEMA_VERSION_PROP);
                if (!SYMBOL_METADATA_SCHEMA_VERSION.equals(existingVersion)) {
                    LOG.warn("Table {} has schema version '{}', expected '{}' — manual migration required",
                            tableId, existingVersion, SYMBOL_METADATA_SCHEMA_VERSION);
                }
                LOG.info("Table {} already exists at schema version {} — skipping creation", tableId, existingVersion);
                return;
            }

            LOG.info("Creating symbol_metadata table: {}", tableId);

            // Define the symbol metadata schema
            Schema schema = new Schema(
                // Primary key fields
                required(1, "exchange_id", Types.StringType.get(), "Exchange identifier (e.g., BINANCE)"),
                required(2, "market_id", Types.StringType.get(), "Market symbol (e.g., BTC/USDT)"),

                // Market information
                optional(3, "market_type", Types.StringType.get(), "Market type (spot, futures, swap, CryptoPerpetual)"),
                optional(4, "description", Types.StringType.get(), "Human-readable description"),
                optional(5, "base_asset", Types.StringType.get(), "Base asset (e.g., BTC)"),
                optional(6, "quote_asset", Types.StringType.get(), "Quote asset (e.g., USDT)"),

                // Supported timeframes
                optional(10, "supported_period_seconds", Types.ListType.ofRequired(11, Types.IntegerType.get()), "Supported OHLC periods in seconds"),

                // Optional timing information
                optional(12, "earliest_time", Types.LongType.get(), "Earliest available data timestamp (nanoseconds since epoch)"),

                // Metadata
                required(13, "updated_at", Types.LongType.get(), "Timestamp when metadata was last updated (nanoseconds since epoch)"),

                // Nautilus Instrument fields — required for constructing Instrument objects in the sandbox bridge
                optional(14, "price_precision", Types.IntegerType.get(), "Decimal places for prices (e.g., 2 means $0.01 resolution)"),
                optional(15, "size_precision", Types.IntegerType.get(), "Decimal places for quantities"),
                optional(16, "tick_size", Types.DoubleType.get(), "Minimum price increment (e.g., 0.01)"),
                optional(17, "lot_size", Types.DoubleType.get(), "Minimum order size"),
                optional(18, "min_notional", Types.DoubleType.get(), "Minimum order value in quote currency"),
                optional(19, "margin_init", Types.DoubleType.get(), "Initial margin requirement (futures/perps only)"),
                optional(20, "margin_maint", Types.DoubleType.get(), "Maintenance margin (futures/perps only)"),
                optional(21, "maker_fee", Types.DoubleType.get(), "Maker fee rate (e.g., 0.001 = 0.1%)"),
                optional(22, "taker_fee", Types.DoubleType.get(), "Taker fee rate"),
                optional(23, "contract_multiplier", Types.DoubleType.get(), "Contract multiplier for derivatives (default 1.0)")
            );

            // Create the table with partitioning and properties
            // Use format version 2 with UPSERT capabilities via equality deletes
            Table table = catalog.buildTable(tableId, schema)
                    .withPartitionSpec(org.apache.iceberg.PartitionSpec.builderFor(schema)
                            .identity("exchange_id")
                            .build())
                    .withProperty("write.format.default", "parquet")
                    .withProperty("write.parquet.compression-codec", "snappy")
                    .withProperty("write.metadata.compression-codec", "gzip")
                    .withProperty("format-version", "2")
                    .withProperty("write.upsert.enabled", "true")
                    .withProperty(SCHEMA_VERSION_PROP, SYMBOL_METADATA_SCHEMA_VERSION)
                    .create();

            // Add identifier fields for UPSERT operations
            // This allows Iceberg to use equality deletes for deduplication
            table.updateProperties()
                    .set("write.upsert.enabled", "true")
                    .commit();

            // Set the identifier fields (primary key) for the table
            // Iceberg will use these for equality deletes during UPSERT
            table.updateSchema()
                    .setIdentifierFields("exchange_id", "market_id")
                    .commit();

            LOG.info("Successfully created symbol_metadata table: {}", tableId);

        } catch (Exception e) {
            LOG.error("Failed to initialize symbol_metadata table: {}", tableId, e);
            throw new RuntimeException("symbol_metadata table initialization failed", e);
        }
    }
}
