/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.component.aws.ddbstream;

import java.math.BigInteger;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreams;
import com.amazonaws.services.dynamodbv2.model.DescribeStreamRequest;
import com.amazonaws.services.dynamodbv2.model.DescribeStreamResult;
import com.amazonaws.services.dynamodbv2.model.GetShardIteratorRequest;
import com.amazonaws.services.dynamodbv2.model.GetShardIteratorResult;
import com.amazonaws.services.dynamodbv2.model.ListStreamsRequest;
import com.amazonaws.services.dynamodbv2.model.ListStreamsResult;
import com.amazonaws.services.dynamodbv2.model.Shard;
import com.amazonaws.services.dynamodbv2.model.ShardIteratorType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class ShardIteratorHandler {
    private static final Logger LOG = LoggerFactory.getLogger(ShardIteratorHandler.class);

    private final DdbStreamEndpoint endpoint;
    private final ShardList shardList = new ShardList();

    private String currentShardIterator;
    private Shard currentShard;

    ShardIteratorHandler(DdbStreamEndpoint endpoint) {
        this.endpoint = endpoint;
    }

    String getShardIterator(String resumeFromSequenceNumber) {
        ShardIteratorType iteratorType = getEndpoint().getConfiguration().getIteratorType();
        String sequenceNumber = getEndpoint().getSequenceNumber();
        if (resumeFromSequenceNumber != null) {
            // Reset things as we're in an error condition.
            currentShard = null;
            currentShardIterator = null;
            iteratorType = ShardIteratorType.AFTER_SEQUENCE_NUMBER;
            sequenceNumber = resumeFromSequenceNumber;
        }
        // either return a cached one or get a new one via a GetShardIterator request.
        if (currentShardIterator == null) {
            ListStreamsResult streamsListResult = getClient().listStreams(
                    new ListStreamsRequest().withTableName(getEndpoint().getConfiguration().getTableName())
            );
            final String streamArn = streamsListResult.getStreams().get(0).getStreamArn(); // XXX assumes there is only one stream
            DescribeStreamResult streamDescriptionResult = getClient().describeStream(
                    new DescribeStreamRequest().withStreamArn(streamArn)
            );
            shardList.addAll(streamDescriptionResult.getStreamDescription().getShards());

            LOG.trace("Current shard is: {} (in {})", currentShard, shardList);
            if (currentShard == null) {
                currentShard = resolveNewShard(iteratorType, resumeFromSequenceNumber);
            } else {
                currentShard = shardList.nextAfter(currentShard);
            }
            shardList.removeOlderThan(currentShard);
            LOG.trace("Next shard is: {} (in {})", currentShard, shardList);

            GetShardIteratorResult result = getClient().getShardIterator(
                    buildGetShardIteratorRequest(streamArn, iteratorType, sequenceNumber)
            );
            currentShardIterator = result.getShardIterator();
        }
        LOG.trace("Shard Iterator is: {}", currentShardIterator);
        return currentShardIterator;
    }

    private GetShardIteratorRequest buildGetShardIteratorRequest(final String streamArn, ShardIteratorType iteratorType, String sequenceNumber) {
        GetShardIteratorRequest req = new GetShardIteratorRequest()
                .withStreamArn(streamArn)
                .withShardId(currentShard.getShardId())
                .withShardIteratorType(iteratorType);
        switch (iteratorType) {
        case AFTER_SEQUENCE_NUMBER:
        case AT_SEQUENCE_NUMBER:
            // if you request with a sequence number that is LESS than the
            // start of the shard, you get a HTTP 400 from AWS.
            // So only add the sequence number if the endpoints
            // sequence number is less than or equal to the starting
            // sequence for the shard.
            // Otherwise change the shart iterator type to trim_horizon
            // because we get a 400 when we use one of the
            // {at,after}_sequence_number iterator types and don't supply
            // a sequence number.
            if (BigIntComparisons.Conditions.LTEQ.matches(
                    new BigInteger(currentShard.getSequenceNumberRange().getStartingSequenceNumber()),
                    new BigInteger(sequenceNumber)
            )) {
                req = req.withSequenceNumber(sequenceNumber);
            } else {
                req = req.withShardIteratorType(ShardIteratorType.TRIM_HORIZON);
            }
            break;
        default:
        }
        return req;
    }

    private Shard resolveNewShard(ShardIteratorType type, String resumeFrom) {
        switch(type) {
        case AFTER_SEQUENCE_NUMBER:
            return shardList.afterSeq(resumeFrom != null ? resumeFrom : getEndpoint().getSequenceNumber());
        case AT_SEQUENCE_NUMBER:
            return shardList.atSeq(getEndpoint().getSequenceNumber());
        case TRIM_HORIZON:
            return shardList.first();
        case LATEST:
        default:
            return shardList.last();
        }
    }

    void updateShardIterator(String nextShardIterator) {
        this.currentShardIterator = nextShardIterator;
    }

    DdbStreamEndpoint getEndpoint() {
        return endpoint;
    }
   
    private AmazonDynamoDBStreams getClient() {
        return getEndpoint().getClient();
    }
}
