package ru.yandex.webmaster3.api.turbo.action.v31;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID;

import NWebmaster.proto.turbo.TurboApi;
import com.datastax.driver.core.utils.UUIDs;
import com.google.protobuf.ByteString;
import lombok.RequiredArgsConstructor;
import org.eclipse.jetty.server.Request;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.XMLReaderFactory;

import ru.yandex.autodoc.common.doc.annotation.Description;
import ru.yandex.webmaster3.api.http.auth.ActionPermission;
import ru.yandex.webmaster3.api.http.auth.Permission;
import ru.yandex.webmaster3.api.http.rest.request.meta.AbstractValidatingHeadersAction;
import ru.yandex.webmaster3.api.turbo.action.TurboPushMode;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.host.service.HostOwnerService;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.logbroker.writer.LogbrokerClient;
import ru.yandex.webmaster3.core.metrics.Category;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboApiTaskWithResult;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboCrawlState;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.storage.turbo.dao.api.TurboApiCustomSettingsYDao;
import ru.yandex.webmaster3.storage.turbo.dao.api.TurboApiHostTasksYDao;
import ru.yandex.webmaster3.storage.turbo.dao.api.TurboApiSettings;

/**
 * Created by Oleg Bazdyrev on 21/08/2017.
 */
@Category("turbo")
@Description("Добавить турбо страницы")
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@ActionPermission(Permission.TURBO)
public class AddTurboPagesV31Action extends AbstractValidatingHeadersAction<AddTurboPagesV31Request, AddTurboPagesV31Response> {
    private static final Logger log = LoggerFactory.getLogger(AddTurboPagesV31Action.class);

    public static final int LIMIT_REQUESTS_PER_OWNER = 100;
    public static final int LIMIT_REQUEST_ENTITY_SIZE = 10 * 1024 * 1024;//взято исходя из лимитов в логброкере

    private final HostOwnerService hostOwnerService;
    private final LogbrokerClient turboLogbrokerClient;
    private final TurboApiCustomSettingsYDao turboApiCustomSettingsYDao;
    private final TurboApiHostTasksYDao turboApiHostTasksYDao;

    @Override
    public AddTurboPagesV31Response validateRequestHeaders(Request httpRequest, AddTurboPagesV31Request requestObject) {
        if (httpRequest.getContentLength() > LIMIT_REQUEST_ENTITY_SIZE) {
            return new AddTurboPagesV31Response.RequestEntityTooLarge("Request entity size > 10 mb");
        }
        try {
            UUID pushId = UUIDs.timeBased();
            WebmasterHostId hostId = requestObject.getLocator().getHostId();
            TurboApiSettings settings = Optional.ofNullable(turboApiCustomSettingsYDao.getSettingsCached(hostId)).orElse(TurboApiSettings.DEFAULT_SETTINGS);
            log.info("User {}, host {}, mode {}, id {}", requestObject.getLocator().getUserId(), hostId, requestObject.getLocator().getMode(), pushId);
            DateTime now = DateTime.now();
            int countUnprocessed = turboApiHostTasksYDao.getHostUnprocessedTaskCount(hostId).intValue();
            // optimization for drive2
            int countOwnerUnprocessed;
            if (settings.getTasksInProgressLimit() > LIMIT_REQUESTS_PER_OWNER) {
                // no check
                countOwnerUnprocessed = 0;
            } else {
                countOwnerUnprocessed = turboApiHostTasksYDao.getOwnerUnprocessedTaskCount(hostId).intValue();
            }
            int ownerTasksLimit = Math.max(LIMIT_REQUESTS_PER_OWNER, settings.getTasksInProgressLimit());
            boolean rpsLimitExceeded = false;
            if (settings.getTasksPerSecondLimit() > 0) {
                TurboApiTaskWithResult task = turboApiHostTasksYDao.getLastTask(hostId);
                if (task != null) {
                    rpsLimitExceeded = !task.getAddDate().isBefore(now.minusMillis(1000 / settings.getTasksPerSecondLimit()));
                }
            }

            if (rpsLimitExceeded) {
                return new AddTurboPagesV31Response.TooManyRequestsError("Limit on requests per second exceeded");
            } else if (countUnprocessed >= settings.getTasksInProgressLimit() || countOwnerUnprocessed >= ownerTasksLimit) {
                return new AddTurboPagesV31Response.TooManyRequestsError("Limit on unprocessed requests exceeded");
            }
        } catch (Exception e) {
            throw new WebmasterException("Failed to validate turbo request", new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), null), e);
        }
        return null;
    }

    @Override
    public AddTurboPagesV31Response process(AddTurboPagesV31Request request) {
        try {
            UUID pushId = UUIDs.timeBased();
            log.info("User {}, host {}, mode {}, id {}",
                    request.getLocator().getUserId(), request.getLocator().getHostId(), request.getLocator().getMode(), pushId);
            byte[] content = request.getEntity().getContent();
            DateTime now = DateTime.now();

            if (!isValidXml(content)) {
                String entityPrefix = tryWrapBytesToString(content, 64);
                String msg = "Expected valid xml" + (entityPrefix == null ? "" : ", found: \"" + entityPrefix + "...\"");
                return new AddTurboPagesV31Response.EntityValidationError(msg);
            }

            boolean productionMode = (request.getLocator().getMode() != TurboPushMode.DEBUG);
            TurboApi.TApiBatchMessage message = TurboApi.TApiBatchMessage.newBuilder()
                    .setId(pushId.toString())
                    .setHost(IdUtils.hostIdToUrl(request.getHostId()))
                    .setDebug(!productionMode)
                    .setFeedContent(ByteString.copyFrom(content))
                    .setUploadTs(System.currentTimeMillis())
                    .build();
            RetryUtils.execute(RetryUtils.linearBackoff(3, Duration.standardSeconds(10)), () -> {
                turboLogbrokerClient.write(message.toByteArray(), Duration.standardSeconds(20));
            });

            turboApiHostTasksYDao.add(
                    TurboApiTaskWithResult.builder()
                            .owner(hostOwnerService.getHostOwner(IdUtils.hostIdToUrl(request.getHostId())))
                            .hostId(request.getHostId())
                            .addDate(new DateTime(UUIDs.unixTimestamp(pushId)))
                            .taskId(pushId)
                            .active(productionMode)
                            .state(TurboCrawlState.PROCESSING)
                            .build()
            );

            return new AddTurboPagesV31Response.NormalResponse(request.getUserId(), request.getHostId(), pushId);
        } catch (Exception e) {
            throw new WebmasterException("Failed to add turbo pages", new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), null), e);
        }
    }

    private String tryWrapBytesToString(byte[] data, int length) {
        try {
            length = Math.min(length, data.length);
            return new String(data, 0, length, StandardCharsets.US_ASCII);
        } catch (Exception e) {
            return null;
        }
    }

    private boolean isValidXml(byte[] data) {
        try {
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            XMLReaderFactory.createXMLReader().parse(new InputSource(is));
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
