package ru.yandex.webmaster3.storage.mobile.dao;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.springframework.stereotype.Repository;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.mobile.data.ScreenshotResolution;
import ru.yandex.webmaster3.storage.mobile.data.*;
import ru.yandex.webmaster3.storage.util.ydb.AbstractYDao;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.DataMapper;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.Field;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.Fields;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * @author leonidrom
 *
 * TTL 1 день по полю add_date
 */
@Repository
public class UserHostMobileAuditRequestsYDao extends AbstractYDao {
    public static final Duration TTL = Duration.standardDays(1);

    private static final ObjectMapper OM = new ObjectMapper();
    private static final String TABLE_NAME = "user_host_mobile_audit_requests";

    public UserHostMobileAuditRequestsYDao() {
        super(PREFIX_MOBILE, TABLE_NAME);
    }

    public void deleteRequest(WebmasterHostId hostId, UUID requestId) {
        delete()
                .where(F.HOST_ID.eq(hostId))
                .and(F.REQUEST_ID.eq(requestId))
                .execute();
    }

    public void createRequest(long userId, WebmasterHostId hostId, UUID requestId, DateTime date, URL url,
                              ScreenshotResolution resolution, boolean adminRequest) {
        insert(
                F.USER_ID.value(userId),
                F.HOST_ID.value(hostId),
                F.REQUEST_ID.value(requestId),
                F.ADD_DATE.value(DateTime.now()),
                F.LAST_UPDATE.value(date),
                F.URL.value(url.toExternalForm()),
                F.STATE.value(MobileAuditRequestState.NEW),
                F.SCREENSHOT_WIDTH.value(resolution.getWidth()),
                F.SCREENSHOT_HEIGHT.value(resolution.getHeight()),
                F.ADMIN_REQUEST.value(adminRequest)
        )
                .execute();
    }

    public void updateRequest(WebmasterHostId hostId, UUID requestId, MobileAuditRequestState state) {
        update()
                .with(F.STATE.set(state))
                .and(F.LAST_UPDATE.set(DateTime.now()))
                .where(F.REQUEST_ID.eq(requestId))
                .and(F.HOST_ID.eq(hostId))
                .execute();
    }

    public void finishRequest(WebmasterHostId hostId, UUID requestId, MobileAuditResultType resultType,
                              MobileAuditResult result)  {
        update()
                .with(F.STATE.set(MobileAuditRequestState.TASK_FINISHED))
                .and(F.LAST_UPDATE.set(DateTime.now()))
                .and(F.RESULT_TYPE.set(resultType))
                .and(F.RESULT.set(serializeResult(result)))
                .where(F.REQUEST_ID.eq(requestId))
                .and(F.HOST_ID.eq(hostId))
                .execute();
    }

    public void deleteForUser(long userId) {
        var sel = select(PK_MAPPER).secondaryIndex("user_id_index").where(F.USER_ID.eq(userId)).getStatement();
        delete().on(sel).execute();
    }

    public MobileAuditRequestInfo getRequest(WebmasterHostId hostId, UUID requestId) {
        return select(MAPPER)
                .where(F.HOST_ID.eq(hostId))
                .and(F.REQUEST_ID.eq(requestId))
                .queryOne();
    }

    public MobileAuditRequestInfo getRequest(UUID requestId) {
        return select(MAPPER).secondaryIndex("request_id_index")
                .where(F.REQUEST_ID.eq(requestId))
                .queryOne();
    }

    public List<MobileAuditRequestInfo> getAllHostRequestsByCreationDesc(WebmasterHostId hostId, DateTime fromDate) {
        var reqs = select(MAPPER)
                .where(F.HOST_ID.eq(hostId))
                .queryForList();

        return reqs.stream()
                .filter(r -> r.getCreatedAt().isEqual(fromDate) || r.getCreatedAt().isAfter(fromDate))
                .sorted(Comparator.comparing(MobileAuditRequestInfo::getCreatedAt).reversed())
                .collect(Collectors.toList());
    }

    private static String serializeResult(MobileAuditResult result) {
        try {
            return OM.writeValueAsString(result);
        } catch (IOException e) {
            throw new WebmasterException("Failed to serialize mobile audit result to DB",
                    new WebmasterErrorResponse.InternalUnknownErrorResponse(UserHostMobileAuditRequestsYDao.class, null),
                    e);
        }
    }

    private static MobileAuditResult parseResult(MobileAuditRequestState state, MobileAuditResultType resultType, String resultString) {
        if (state == MobileAuditRequestState.TASK_FINISHED) {
            try {
                switch (resultType) {
                    case OK:
                        return OM.readValue(resultString, MobileAuditResult.Success.class);
                    case INTERNAL_ERROR:
                        return OM.readValue(resultString, MobileAuditResult.InternalError.class);
                    case FETCH_FAILED:
                        return OM.readValue(resultString, MobileAuditResult.FetchFailedError.class);
                    default:
                        throw new WebmasterException("Unknown mobile audit result type " + resultType,
                                new WebmasterErrorResponse.InternalUnknownErrorResponse(UserHostMobileAuditRequestsYDao.class, null));
                }

            } catch (IOException e) {
                throw new WebmasterException("Failed to deserialize " + resultType + " mobile audit result from DB",
                        new WebmasterErrorResponse.InternalUnknownErrorResponse(UserHostMobileAuditRequestsYDao.class, null), e);
            }
        } else {
            return null;
        }
    }

     private static final DataMapper<MobileAuditRequestInfo> MAPPER = DataMapper.create(
            F.USER_ID, F.HOST_ID, F.URL, F.REQUEST_ID, F.LAST_UPDATE, F.STATE, F.RESULT_TYPE, F.RESULT,
            F.SCREENSHOT_WIDTH, F.SCREENSHOT_HEIGHT, F.ADMIN_REQUEST,
            (userId, hostId, urlStr, requestId, lastUpdate, state, resultType, resultString, width, height, adminRequest) -> {
                MobileAuditResult result = parseResult(state, resultType, resultString);
                ScreenshotResolution resolution = new ScreenshotResolution(width, height);
                URL url;
                try {
                    url = new java.net.URL(urlStr);
                } catch (MalformedURLException e) {
                    throw new WebmasterException("Failed to read url from DB", new WebmasterErrorResponse.InternalUnknownErrorResponse(UserHostMobileAuditRequestsYDao.class, ""), e);
                }

                return new MobileAuditRequestInfo(userId, hostId, url, resolution, requestId, lastUpdate, state, result, adminRequest);
            }
    );

    private static DataMapper<Pair<WebmasterHostId, UUID>> PK_MAPPER = DataMapper.create(F.HOST_ID, F.REQUEST_ID, Pair::of);

    private static final class F {
        static final Field<WebmasterHostId> HOST_ID = Fields.hostIdField("host_id");
        static final Field<UUID> REQUEST_ID = Fields.uuidField("request_id");
        static final Field<Long> USER_ID = Fields.longField("user_id");
        static final Field<String> URL = Fields.stringField("url");
        static final Field<DateTime> ADD_DATE = Fields.jodaDateTimeField("add_date");
        static final Field<DateTime> LAST_UPDATE = Fields.jodaDateTimeField("last_update");
        static final Field<MobileAuditRequestState> STATE = Fields.intEnumField("state", MobileAuditRequestState.R);
        static final Field<MobileAuditResultType> RESULT_TYPE = Fields.intEnumField("result_type", MobileAuditResultType.R).makeOptional();
        static final Field<String> RESULT = Fields.stringRawField("result").makeOptional();
        static final Field<Integer> SCREENSHOT_WIDTH = Fields.intField("screenshot_width").withDefault(320);
        static final Field<Integer> SCREENSHOT_HEIGHT = Fields.intField("screenshot_height").withDefault(460);
        static final Field<Boolean> ADMIN_REQUEST = Fields.boolField("admin_request").withDefault(false);
    }
}
