package io.delphiplatform.api.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.bigtable.v2.ReadRowsRequest;
import com.google.cloud.bigtable.data.v2.internal.RequestContext;
import com.google.cloud.bigtable.data.v2.models.Query;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.test.context.util.TestContextResourceUtils;
import org.testcontainers.shaded.org.apache.commons.io.FileUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@Component
public class TestUtils {

    private static ObjectMapper objectMapper = new ObjectMapper();
    private static ApplicationContext applicationContext;

    @Autowired
    private ApplicationContext applicationContextBean;

    @PostConstruct
    public void init() {
        applicationContext = applicationContextBean;
    }

    public static List<Pair<String, String>> getRanges(Query query) {
        ReadRowsRequest readRowsRequest = getReadRowsRequest(query);
        return readRowsRequest.getRows().getRowRangesList().stream().map(range ->
                Pair.of(
                    firstNotEmpty(range.getStartKeyOpen().toStringUtf8(), range.getStartKeyClosed().toStringUtf8()),
                    firstNotEmpty(range.getEndKeyOpen().toStringUtf8(), range.getEndKeyClosed().toStringUtf8())
                ))
            .collect(Collectors.toList());
    }

    public static long getLimit(Query query) {
        ReadRowsRequest readRowsRequest = getReadRowsRequest(query);
        return readRowsRequest.getRowsLimit();
    }

    public static String reverseString(String string) {
        return new StringBuilder(string).reverse().toString();
    }

    public static List<Resource> getClasspathResources(String... filePaths) {
        String[] paths = TestContextResourceUtils.convertToClasspathResourcePaths(TestUtils.class, filePaths);

        return TestContextResourceUtils.convertToResourceList(applicationContext, paths);
    }

    public static List<File> getClasspathFiles(String... filePaths) {
        return getClasspathResources(filePaths)
            .stream().map(resource -> {
                try {
                    return resource.getFile();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }).collect(Collectors.toList());
    }

    public static String readFileAsString(File file) {
        try {
            return FileUtils.readFileToString(file, "UTF-8");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static Document convertStringToXMLDocument(String xmlString) {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        try {
            DocumentBuilder builder = factory.newDocumentBuilder();

            return builder.parse(new InputSource(new StringReader(xmlString)));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Get ordered list of all "data seed" migrations from liquibase changelog file
     * <p>
     * Method parses db.changelog-master.xml file and distinguishes "seed" migrations based on comment markers -
     * <p>
     * <!-- Seed migrations -->, <!-- END Seed migrations -->
     */
    public static List<String> getDataSeedMigrationsFilePaths() throws Exception {
        List<String> seedMigrationFilePaths = new ArrayList<>();

        String dbChangelogXmlStr = readFileAsString(
            getClasspathFiles(String.format("/%s/%s", Constants.DB_MIGRATIONS_DIR, Constants.DB_MIGRATIONS_LOG_FILE))
                .get(0)
        );

        String seedMigrationsListXmlChunkStr = dbChangelogXmlStr.substring(
            dbChangelogXmlStr.indexOf("<!-- Seed migrations -->"),
            dbChangelogXmlStr.indexOf("<!-- END Seed migrations -->")
        );

        String seedMigrationsListXmlStr = String.format("<fakeXmlRootTag>%s</fakeXmlRootTag>", seedMigrationsListXmlChunkStr);

        Document seedMigrationsListXml = convertStringToXMLDocument(seedMigrationsListXmlStr);

        NodeList childNodes = seedMigrationsListXml.getDocumentElement().getChildNodes();

        for (int i = 0; i < childNodes.getLength(); i++) {
            Node node = childNodes.item(i);

            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element element = (Element) node;

                if ("include".equals(element.getTagName())) {
                    String migrationFilePath = element.getAttribute("file");

                    seedMigrationFilePaths.add(migrationFilePath);

                } else if ("changeSet".equals(element.getTagName())) {
                    NodeList changeSetChildNodes = element.getChildNodes();

                    for (int ii = 0; ii < changeSetChildNodes.getLength(); ii++) {
                        Node changeSetChildNode = changeSetChildNodes.item(ii);

                        if (changeSetChildNode.getNodeType() == Node.ELEMENT_NODE) {
                            Element sqlFileElem = (Element) changeSetChildNode;

                            if ("sqlFile".equals(sqlFileElem.getTagName())) {
                                String migrationFilePath = sqlFileElem.getAttribute("path");

                                seedMigrationFilePaths.add(migrationFilePath);
                            }

                        }
                    }
                } else {
                    throw new RuntimeException("unknown tag inside DB changelog (seed data): " + element.getTagName());
                }
            }
        }

        return seedMigrationFilePaths;
    }

    private static ReadRowsRequest getReadRowsRequest(Query query) {
        RequestContext requestContext = mock(RequestContext.class);
        when(requestContext.getAppProfileId()).thenReturn("testAppId");
        when(requestContext.getInstanceId()).thenReturn("testInstanceId");
        when(requestContext.getProjectId()).thenReturn("testProjectId");
        return query.toProto(requestContext);
    }

    private static String firstNotEmpty(String... strings) {
        for (String string : strings) {
            if (StringUtils.isNotEmpty(string)) {
                return string;
            }
        }
        return null;
    }

    public static <T> T deepCopyObject(T orig, Class<T> clazz) {
        try {
            return objectMapper
                .readValue(objectMapper.writeValueAsString(orig), clazz);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
