package io.sinistral.proteus.swagger.services;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.typesafe.config.Config;
import io.sinistral.proteus.server.endpoints.EndpointInfo;
import io.sinistral.proteus.services.DefaultService;
import io.sinistral.proteus.swagger.jaxrs2.Reader;
import io.sinistral.proteus.swagger.jaxrs2.ServerParameterExtension;
import io.swagger.jaxrs.ext.SwaggerExtension;
import io.swagger.jaxrs.ext.SwaggerExtensions;
import io.swagger.models.Info;
import io.swagger.models.Swagger;
import io.swagger.util.Json;
import io.swagger.util.Yaml;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.RoutingHandler;
import io.undertow.server.handlers.ResponseCodeHandler;
import io.undertow.server.handlers.resource.FileResourceManager;
import io.undertow.server.handlers.resource.ResourceHandler;
import io.undertow.util.CanonicalPathUtils;
import io.undertow.util.Headers;
import io.undertow.util.Methods;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MediaType;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.jar.JarFile;

/**
 * A service for generating and serving an Swagger 2.0 spec and ui.
 * @author jbauer
 */

@Singleton
public class SwaggerService extends DefaultService implements Supplier<RoutingHandler>
{

	private static Logger log = LoggerFactory.getLogger(SwaggerService.class.getCanonicalName());

	protected Reader reader = null;

	protected final String resourcePathPrefix = "swagger";

	protected final String resourcePrefix = "io/sinistral/proteus/swagger";

	@Inject
	@Named("swagger.basePath")
	protected String basePath;

	@Inject
	@Named("swagger.theme")
	protected String theme;

	@Inject
	@Named("swagger.specFilename")
	protected String specFilename;

	@Inject
	@Named("swagger")
	protected Config swaggerConfig;

	@Inject
	@Named("swagger.security")
	protected Config securityConfig;

	@Inject
	@Named("swagger.redocPath")
	protected String redocPath;

	@Inject
	@Named("swagger.host")
	protected String host;

	@Inject
	@Named("application.name")
	protected String applicationName;

	@Inject
	@Named("swagger.port")
	protected Integer port;

	@Inject
	@Named("application.path")
	protected String applicationPath;

	@Inject
	protected RoutingHandler router;

	@Inject
	@Named("registeredEndpoints")
	protected Set<EndpointInfo> registeredEndpoints;

	@Inject
	@Named("registeredControllers")
	protected Set<Class<?>> registeredControllers;

	@Inject
	@Named("registeredHandlerWrappers")
	protected Map<String, HandlerWrapper> registeredHandlerWrappers;

	protected ObjectMapper mapper;

	protected ObjectWriter writer;

	protected ObjectMapper yamlMapper;

	protected Path swaggerResourcePath = null;

	protected ClassLoader serviceClassLoader = null;

	protected Swagger swagger = null;

	protected String swaggerSpec = null;

	protected String swaggerIndexHTML = null;

	protected String redocHTML = null;

	public SwaggerService()
	{
		mapper = Json.mapper();

		writer = mapper.writerWithDefaultPrettyPrinter();
		writer = writer.without(SerializationFeature.WRITE_NULL_MAP_VALUES);
		yamlMapper = Yaml.mapper();
	}

	public void generateSwaggerSpec() throws Exception
	{

		Set<Class<?>> classes = this.registeredControllers;
		
		log.debug("registeredControllers: " + this.registeredControllers);

		List<SwaggerExtension> extensions = new ArrayList<>();

		extensions.add(new ServerParameterExtension());

		SwaggerExtensions.setExtensions(extensions);

		log.debug("Added SwaggerExtension: ServerParameterExtension");

		Swagger swagger = new Swagger();

		swagger.setBasePath(applicationPath);

		swagger.setHost(host + ((port != 80 && port != 443) ? ":" + port : ""));

		Info info = mapper.convertValue(swaggerConfig.getValue("info").unwrapped(), Info.class);

		swagger.setInfo(info);

//		if (securityConfig.hasPath("apiKeys"))
//		{
//			List<? extends ConfigObject> apiKeys = securityConfig.getObjectList("apiKeys");
//
//			for (ConfigObject apiKey : apiKeys)
//			{
//				Config apiKeyConfig = apiKey.toConfig();
//
//				String key = apiKeyConfig.getString("key");
//				String name = apiKeyConfig.getString("name");
//				String value = apiKeyConfig.getString("value");
//
//				io.swagger.converters.auth.In keyLocation = io.swagger.converters.auth.In.valueOf(apiKeyConfig.getString("in"));
//
//				final Predicate predicate;
//
//				switch (keyLocation)
//				{
//					case HEADER:
//					{
//						ExchangeAttribute[] attributes = new ExchangeAttribute[] { ExchangeAttributes.requestHeader(HttpString.tryFromString(name)),
//								ExchangeAttributes.constant(value) };
//						predicate = Predicates.equals(attributes);
//						break;
//					}
//					case QUERY:
//					{
//						predicate = Predicates.contains(ExchangeAttributes.queryString(), value);
//						break;
//					}
//					default:
//						predicate = Predicates.truePredicate();
//						break;
//				}
//
//				if (predicate != null)
//				{
//					log.debug("Adding apiKey handler " + name + " in " + keyLocation + " named " + key);
//
//					final HandlerWrapper wrapper = new HandlerWrapper()
//					{
//						@Override
//						public HttpHandler wrap(final HttpHandler handler)
//						{
//							return new PredicateHandler(predicate, handler, ResponseCodeHandler.HANDLE_403);
//						}
//					};
//
//					ApiKeyAuthDefinition keyAuthDefinition = new ApiKeyAuthDefinition(name, keyLocation);
//					swagger.addSecurityDefinition(key, keyAuthDefinition);
//
//					registeredHandlerWrappers.put(key, wrapper);
//				}
//			}
//		}
//
//		if (securityConfig.hasPath("basicRealms"))
//		{
//			List<? extends ConfigObject> realms = securityConfig.getObjectList("basicRealms");
//
//			for (ConfigObject realm : realms)
//			{
//				Config realmConfig = realm.toConfig();
//
//				final String name = realmConfig.getString("name");
//
//				List<String> identities = realmConfig.getStringList("identities");
//
//				final Map<String, char[]> identityMap = new HashMap<>();
//
//				identities.stream().forEach(i ->
//				{
//					String[] identity = i.split(":");
//
//					identityMap.put(identity[0], identity[1].toCharArray());
//				});
//
//				final IdentityManager identityManager = new MapIdentityManager(identityMap);
//
//				log.debug("Adding basic handler for realm " + name + " with identities " + identityMap);
//
//				final HandlerWrapper wrapper = new HandlerWrapper()
//				{
//					@Override
//					public HttpHandler wrap(final HttpHandler handler)
//					{
//						HttpHandler authHandler = new AuthenticationCallHandler(handler);
//						authHandler = new AuthenticationConstraintHandler(authHandler);
//						final List<AuthenticationMechanism> mechanisms = Collections.<AuthenticationMechanism> singletonList(new BasicAuthenticationMechanism(name));
//						authHandler = new AuthenticationMechanismsHandler(authHandler, mechanisms);
//						authHandler = new SecurityInitialHandler(AuthenticationMode.PRO_ACTIVE, identityManager, authHandler);
//						return authHandler;
//					}
//				};
//
//				BasicAuthDefinition authDefinition = new BasicAuthDefinition();
//				swagger.addSecurityDefinition(name, authDefinition);
//
//				registeredHandlerWrappers.put(name, wrapper);
//
//			}
//		}

		this.reader = new Reader(swagger);

		classes.forEach(c -> this.reader.read(c));

		this.swagger = this.reader.getSwagger();
		
		this.swaggerSpec = writer.writeValueAsString(swagger);

	}

	public Swagger getSwagger()
	{
		return swagger;
	}

	public void setSwagger(Swagger swagger)
	{
		this.swagger = swagger;
	}

	protected void generateSwaggerHTML()
	{
		try
		{

			try (InputStream templateInputStream = getClass().getClassLoader().getResourceAsStream(resourcePrefix + "/index.html"))
			{
				byte[] templateBytes = IOUtils.toByteArray(templateInputStream);

				String templateString = new String(templateBytes, Charset.defaultCharset());

				String themePath = "swagger-ui.css";

				if (!theme.equals("default"))
				{
					themePath = "themes/theme-" + theme + ".css";
				}

				templateString = templateString.replaceAll("\\{\\{ themePath }}", themePath);
				templateString = templateString.replaceAll("\\{\\{ swaggerBasePath }}", basePath);
				templateString = templateString.replaceAll("\\{\\{ title }}", applicationName + " Swagger UI");
				templateString = templateString.replaceAll("\\{\\{ swaggerFilePath }}", basePath + ".json");

				this.swaggerIndexHTML = templateString;
			}

			try (InputStream templateInputStream = getClass().getClassLoader().getResourceAsStream(resourcePrefix + "/redoc.html"))
			{
				byte[] templateBytes = IOUtils.toByteArray(templateInputStream);

				String templateString = new String(templateBytes, Charset.defaultCharset());

				templateString = templateString.replaceAll("\\{\\{ specPath }}", this.basePath + ".json");
				templateString = templateString.replaceAll("\\{\\{ applicationName }}", applicationName);

				this.redocHTML = templateString;
			}

			URL url = getClass().getClassLoader().getResource(resourcePrefix);

			if (url.toExternalForm().contains("!"))
			{
				log.debug("Copying Swagger resources...");

				String appName = config.getString("application.name").replaceAll(" ", "_");

				Path tmpDirParent = Files.createTempDirectory(appName);

				Path swaggerTmpDir = tmpDirParent.resolve("swagger/");

				String jarPathString = url.toExternalForm().substring(0, url.toExternalForm().indexOf("!")).replaceAll("file:", "").replaceAll("jar:", "");

				File srcFile = new File(jarPathString);

				try (JarFile jarFile = new JarFile(srcFile, false))
				{

					if (swaggerTmpDir.toFile().exists())
					{
						log.debug("Deleting existing Swagger directory at " + swaggerTmpDir);

						try
						{
							FileUtils.deleteDirectory(swaggerTmpDir.toFile());

						} catch (IllegalArgumentException e)
						{
							log.debug("Swagger tmp directory is not a directory...");

							swaggerTmpDir.toFile().delete();
						}
					}

					Files.createDirectory(swaggerTmpDir);

					this.swaggerResourcePath = swaggerTmpDir;

					jarFile.stream().filter(ze -> ze.getName().endsWith("js") || ze.getName().endsWith("css") || ze.getName().endsWith("map") || ze.getName().endsWith("html"))
							.forEach(ze ->
							{

								try
								{
									final InputStream entryInputStream = jarFile.getInputStream(ze);

									String filename = ze.getName().substring(resourcePrefix.length() + 1);

									Path entryFilePath = swaggerTmpDir.resolve(filename);

									Files.createDirectories(entryFilePath.getParent());

									Files.copy(entryInputStream, entryFilePath, StandardCopyOption.REPLACE_EXISTING);

								} catch (Exception e)
								{
									log.error(e.getMessage() + " for entry " + ze.getName());
								}
							});
				}
			}
			else
			{
				this.swaggerResourcePath = Paths.get(this.getClass().getClassLoader().getResource(this.resourcePrefix).toURI());
				this.serviceClassLoader = this.getClass().getClassLoader();
			}

		} catch (Exception e)
		{
			log.error(e.getMessage(), e);
		}
	}

	public RoutingHandler get()
	{

		RoutingHandler router = new RoutingHandler();

		/*
		 * JSON path
		 */

		String pathTemplate = this.basePath + ".json";

		FileResourceManager resourceManager = new FileResourceManager(this.swaggerResourcePath.toFile(), 1024);

 
		router.add(HttpMethod.GET, pathTemplate, (HttpServerExchange exchange) ->
		{
			final Swagger swaggerCopy = swagger;

			exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MediaType.APPLICATION_JSON);

			String spec = null;

			try
			{
				swaggerCopy.setHost(exchange.getHostAndPort());

				spec = writer.writeValueAsString(swaggerCopy);

			} catch (Exception e)
			{
				log.error(e.getMessage(), e);
			}

			exchange.getResponseSender().send(spec);

		});

		this.registeredEndpoints.add(
										EndpointInfo.builder().withConsumes("*/*").withPathTemplate(pathTemplate).withControllerName(this.getClass().getSimpleName())
												.withMethod(Methods.GET).withProduces(MediaType.APPLICATION_JSON).build());

		/*
		 * YAML path
		 */

		pathTemplate = this.basePath + ".yaml";

		router.add(HttpMethod.GET, pathTemplate, (HttpServerExchange exchange) ->
		{
			exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, io.sinistral.proteus.protocol.MediaType.TEXT_YAML.contentType());

				String spec = null;

				final Swagger swaggerCopy = swagger;

				try
				{
					swaggerCopy.setHost(exchange.getHostAndPort());

					spec = yamlMapper.writeValueAsString(swaggerCopy);

				} catch (Exception e)
				{
					log.error(e.getMessage(), e);
				}

				exchange.getResponseSender().send(spec);
		});

		this.registeredEndpoints.add(
										EndpointInfo.builder().withConsumes("*/*").withPathTemplate(pathTemplate).withControllerName(this.getClass().getSimpleName())
                                                    .withMethod(Methods.GET).withProduces(io.sinistral.proteus.protocol.MediaType.TEXT_YAML.contentType()).build());

		pathTemplate = this.basePath + "/" + this.redocPath;

		router.add(HttpMethod.GET, pathTemplate, (HttpServerExchange exchange) ->
		{
			exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MediaType.TEXT_HTML);
			exchange.getResponseSender().send(redocHTML);
		});

		this.registeredEndpoints.add(
										EndpointInfo.builder().withConsumes("*/*").withPathTemplate(pathTemplate).withControllerName(this.getClass().getSimpleName())
												.withMethod(Methods.GET).withProduces(MediaType.TEXT_HTML).build());

		pathTemplate = this.basePath;

		router.add(HttpMethod.GET, pathTemplate, (HttpServerExchange exchange) ->
		{
			exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MediaType.TEXT_HTML);
			exchange.getResponseSender().send(swaggerIndexHTML);
		});

		this.registeredEndpoints.add(
										EndpointInfo.builder().withConsumes(MediaType.WILDCARD).withProduces(MediaType.TEXT_HTML).withPathTemplate(pathTemplate)
												.withControllerName(this.getClass().getSimpleName()).withMethod(Methods.GET).build());

		try
		{

			pathTemplate = this.basePath + "/*";

			router.add(HttpMethod.GET, pathTemplate, new ResourceHandler(resourceManager)
			{

				@Override
				public void handleRequest(HttpServerExchange exchange) throws Exception
				{

					String canonicalPath = CanonicalPathUtils.canonicalize((exchange.getRelativePath()));

					canonicalPath = canonicalPath.split(basePath)[1];

					exchange.setRelativePath(canonicalPath);

					if (serviceClassLoader == null)
					{
						super.handleRequest(exchange);
					}
					else
					{
						canonicalPath = resourcePrefix + canonicalPath;

						try (final InputStream resourceInputStream = serviceClassLoader.getResourceAsStream(canonicalPath))
						{

							if (resourceInputStream == null)
							{
								ResponseCodeHandler.HANDLE_404.handleRequest(exchange);
								return;
							}

							byte[] resourceBytes = IOUtils.toByteArray(resourceInputStream);

							io.sinistral.proteus.protocol.MediaType mediaType = io.sinistral.proteus.protocol.MediaType.getByFileName(canonicalPath);

							exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, mediaType.toString());

							exchange.getResponseSender().send(ByteBuffer.wrap(resourceBytes));
						}
					}

				}

			});

			this.registeredEndpoints.add(
											EndpointInfo.builder().withConsumes(MediaType.WILDCARD).withProduces(MediaType.WILDCARD).withPathTemplate(pathTemplate)
													.withControllerName(this.getClass().getSimpleName()).withMethod(Methods.GET).build());

		} catch (Exception e)
		{
			log.error(e.getMessage(), e);
		}

		return router;
	}

	@Override
	protected void startUp() throws Exception
	{
		super.startUp();

		this.generateSwaggerHTML();

		CompletableFuture.runAsync(() ->
		{

			try
			{

				generateSwaggerSpec();

				log.info("\nSwagger Spec:\n" + writer.writeValueAsString(swagger));

			} catch (Exception e)
			{
				log.error("Error generating swagger spec.", e);
			}

		});

		router.addAll(this.get());
	}

}