package com.qdesrame.openapi.diff.output;

import static com.qdesrame.openapi.diff.model.Changed.result;
import static com.qdesrame.openapi.diff.utils.ChangedUtils.isUnchanged;
import static java.lang.String.format;

import com.qdesrame.openapi.diff.model.*;
import com.qdesrame.openapi.diff.utils.RefPointer;
import com.qdesrame.openapi.diff.utils.RefType;
import io.swagger.v3.oas.models.headers.Header;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MarkdownRender implements Render {

  public static final Logger LOGGER = LoggerFactory.getLogger(MarkdownRender.class);

  protected static RefPointer<Schema> refPointer = new RefPointer<>(RefType.SCHEMAS);
  protected final String H3 = "### ";
  protected final String H4 = "#### ";
  protected final String H5 = "##### ";
  protected final String H6 = "###### ";
  protected final String BLOCKQUOTE = "> ";
  protected final String CODE = "`";
  protected final String PRE_CODE = "    ";
  protected final String PRE_LI = "    ";
  protected final String LI = "* ";
  protected final String HR = "---\n";
  protected ChangedOpenApi diff;

  /**
   * A paramater which indicates whether or not metadata (summary and metadata) changes should be
   * logged in the changelog file.
   */
  @Getter @Setter protected boolean showChangedMetadata;

  public MarkdownRender() {}

  public String render(ChangedOpenApi diff) {
    this.diff = diff;
    return listEndpoints("What's New", diff.getNewEndpoints())
        + listEndpoints("What's Deleted", diff.getMissingEndpoints())
        + listEndpoints("What's Deprecated", diff.getDeprecatedEndpoints())
        + listEndpoints(diff.getChangedOperations());
  }

  protected String sectionTitle(String title) {
    return H4 + title + '\n' + HR + '\n';
  }

  protected String listEndpoints(String title, List<Endpoint> endpoints) {
    if (null == endpoints || endpoints.size() == 0) return "";
    StringBuilder sb = new StringBuilder(sectionTitle(title));
    endpoints
        .stream()
        .map(e -> itemEndpoint(e.getMethod().toString(), e.getPathUrl(), e.getSummary()))
        .forEach(sb::append);
    return sb.toString();
  }

  protected String itemEndpoint(String method, String path, String summary) {
    return H5 + CODE + method + CODE + " " + path + "\n\n" + metadata(summary) + "\n";
  }

  protected String itemEndpoint(String method, String path, ChangedMetadata summary) {
    return H5 + CODE + method + CODE + " " + path + "\n\n" + metadata("summary", summary) + "\n";
  }

  protected String titleH5(String title) {
    return H6 + title + '\n';
  }

  protected String listEndpoints(List<ChangedOperation> changedOperations) {
    if (null == changedOperations || changedOperations.size() == 0) return "";
    StringBuilder sb = new StringBuilder(sectionTitle("What's Changed"));
    changedOperations
        .stream()
        .map(
            operation -> {
              StringBuilder details =
                  new StringBuilder()
                      .append(
                          itemEndpoint(
                              operation.getHttpMethod().toString(),
                              operation.getPathUrl(),
                              operation.getSummary()));
              if (result(operation.getParameters()).isDifferent()) {
                details
                    .append(titleH5("Parameters:"))
                    .append(parameters(operation.getParameters()));
              }
              if (operation.resultRequestBody().isDifferent()) {
                details
                    .append(titleH5("Request:"))
                    .append(metadata("Description", operation.getRequestBody().getDescription()))
                    .append(bodyContent(operation.getRequestBody().getContent()));
              }
              if (operation.resultApiResponses().isDifferent()) {
                details
                    .append(titleH5("Return Type:"))
                    .append(responses(operation.getApiResponses()));
              }
              return details.toString();
            })
        .forEach(sb::append);
    return sb.toString();
  }

  protected String responses(ChangedApiResponse changedApiResponse) {
    StringBuilder sb = new StringBuilder("\n");
    sb.append(listResponse("New response", changedApiResponse.getIncreased()));
    sb.append(listResponse("Deleted response", changedApiResponse.getMissing()));
    changedApiResponse
        .getChanged()
        .entrySet()
        .stream()
        .map(e -> this.itemResponse(e.getKey(), e.getValue()))
        .forEach(sb::append);
    return sb.toString();
  }

  protected String listResponse(String title, Map<String, ApiResponse> responses) {
    StringBuilder sb = new StringBuilder();
    responses
        .entrySet()
        .stream()
        .map(e -> this.itemResponse(title, e.getKey(), e.getValue()))
        .forEach(sb::append);
    return sb.toString();
  }

  protected String itemResponse(String title, String code, ApiResponse response) {
    return this.itemResponse(title, code, response.getDescription());
  }

  protected String itemResponse(String code, ChangedResponse response) {
    StringBuilder sb = new StringBuilder();
    sb.append(
        this.itemResponse(
            "Changed response",
            code,
            null == response.getNewApiResponse()
                ? ""
                : response.getNewApiResponse().getDescription()));
    sb.append(headers(response.getHeaders()));
    if (response.getContent() != null) {
      sb.append(this.bodyContent(LI, response.getContent()));
    }
    return sb.toString();
  }

  protected String itemResponse(String title, String code, String description) {
    StringBuilder sb = new StringBuilder();
    String status = "";
    if (!code.equals("default")) {
      status = HttpStatus.getStatusText(Integer.parseInt(code));
    }
    sb.append(format("%s : **%s %s**\n", title, code, status));
    sb.append(metadata(description));
    return sb.toString();
  }

  protected String headers(ChangedHeaders headers) {
    StringBuilder sb = new StringBuilder();
    if (headers != null) {
      sb.append(listHeader("New header", headers.getIncreased()))
          .append(listHeader("Deleted header", headers.getMissing()));
      headers
          .getChanged()
          .entrySet()
          .stream()
          .map(e -> this.itemHeader(e.getKey(), e.getValue()))
          .forEach(sb::append);
    }
    return sb.toString();
  }

  protected String listHeader(String title, Map<String, Header> headers) {
    StringBuilder sb = new StringBuilder();
    headers
        .entrySet()
        .stream()
        .map(e -> this.itemHeader(title, e.getKey(), e.getValue()))
        .forEach(sb::append);
    return sb.toString();
  }

  protected String itemHeader(String title, String name, Header header) {
    return this.itemHeader(title, name, header.getDescription());
  }

  protected String itemHeader(String code, ChangedHeader header) {
    return this.itemHeader(
        "Changed header",
        code,
        null == header.getNewHeader() ? "" : header.getNewHeader().getDescription());
  }

  protected String itemHeader(String title, String mediaType, String description) {
    return format("%s : `%s`\n\n", title, mediaType) + metadata(description) + '\n';
  }

  protected String bodyContent(String prefix, ChangedContent changedContent) {
    if (changedContent == null) {
      return "";
    }
    StringBuilder sb = new StringBuilder("\n");
    sb.append(listContent(prefix, "New content type", changedContent.getIncreased()));
    sb.append(listContent(prefix, "Deleted content type", changedContent.getMissing()));
    final int deepness;
    if (StringUtils.isNotBlank(prefix)) {
      deepness = 1;
    } else {
      deepness = 0;
    }
    changedContent
        .getChanged()
        .entrySet()
        .stream()
        .map(e -> this.itemContent(deepness, e.getKey(), e.getValue()))
        .forEach(e -> sb.append(prefix).append(e));
    return sb.toString();
  }

  protected String bodyContent(ChangedContent changedContent) {
    return bodyContent("", changedContent);
  }

  protected String listContent(String prefix, String title, Map<String, MediaType> mediaTypes) {
    StringBuilder sb = new StringBuilder();
    mediaTypes
        .entrySet()
        .stream()
        .map(e -> this.itemContent(title, e.getKey(), e.getValue()))
        .forEach(e -> sb.append(prefix).append(e));
    return sb.toString();
  }

  protected String itemContent(String title, String mediaType) {
    return format("%s : `%s`\n\n", title, mediaType);
  }

  protected String itemContent(String title, String mediaType, MediaType content) {
    return itemContent(title, mediaType);
  }

  protected String itemContent(int deepness, String mediaType, ChangedMediaType content) {
    return itemContent("Changed content type", mediaType) + schema(deepness, content.getSchema());
  }

  protected String schema(ChangedSchema schema) {
    return schema(1, schema);
  }

  protected String oneOfSchema(int deepness, ChangedOneOfSchema schema, String discriminator) {
    StringBuilder sb = new StringBuilder();
    schema
        .getMissing()
        .keySet()
        .forEach(
            key -> sb.append(format("%sDeleted '%s' %s\n", indent(deepness), key, discriminator)));
    schema
        .getIncreased()
        .forEach(
            (key, sub) ->
                sb.append(format("%sAdded '%s' %s:\n", indent(deepness), key, discriminator))
                    .append(schema(deepness, sub, schema.getContext())));
    schema
        .getChanged()
        .forEach(
            (key, sub) ->
                sb.append(format("%sUpdated `%s` %s:\n", indent(deepness), key, discriminator))
                    .append(schema(deepness, sub)));
    return sb.toString();
  }

  protected String required(int deepness, String title, List<String> required) {
    StringBuilder sb = new StringBuilder();
    if (required.size() > 0) {
      sb.append(format("%s%s:\n", indent(deepness), title));
      required.forEach(s -> sb.append(format("%s- `%s`\n", indent(deepness), s)));
      sb.append("\n");
    }
    return sb.toString();
  }

  protected String schema(int deepness, ChangedSchema schema) {
    StringBuilder sb = new StringBuilder();
    if (schema.isDiscriminatorPropertyChanged()) {
      LOGGER.debug("Discriminator property changed");
    }
    if (schema.getOneOfSchema() != null) {
      String discriminator =
          schema.getNewSchema().getDiscriminator() != null
              ? schema.getNewSchema().getDiscriminator().getPropertyName()
              : "";
      sb.append(oneOfSchema(deepness, schema.getOneOfSchema(), discriminator));
    }
    if (schema.getRequired() != null) {
      sb.append(required(deepness, "New required properties", schema.getRequired().getIncreased()));
      sb.append(required(deepness, "New optional properties", schema.getRequired().getMissing()));
    }
    if (schema.getItems() != null) {
      sb.append(items(deepness, schema.getItems()));
    }
    sb.append(listDiff(deepness, "enum", schema.getEnumeration()));
    sb.append(
        properties(
            deepness,
            "Added property",
            schema.getIncreasedProperties(),
            true,
            schema.getContext()));
    sb.append(
        properties(
            deepness,
            "Deleted property",
            schema.getMissingProperties(),
            false,
            schema.getContext()));
    schema
        .getChangedProperties()
        .forEach((name, property) -> sb.append(property(deepness, name, property)));
    return sb.toString();
  }

  protected String schema(int deepness, ComposedSchema schema, DiffContext context) {
    StringBuilder sb = new StringBuilder();
    if (schema.getAllOf() != null && schema.getAllOf() != null) {
      LOGGER.debug("All of schema");
      schema
          .getAllOf()
          .stream()
          .map(this::resolve)
          .forEach(composedChild -> sb.append(schema(deepness, composedChild, context)));
    }
    if (schema.getOneOf() != null && schema.getOneOf() != null) {
      LOGGER.debug("One of schema");
      sb.append(format("%sOne of:\n\n", indent(deepness)));
      schema
          .getOneOf()
          .stream()
          .map(this::resolve)
          .forEach(composedChild -> sb.append(schema(deepness + 1, composedChild, context)));
    }
    return sb.toString();
  }

  protected String schema(int deepness, Schema schema, DiffContext context) {
    StringBuilder sb = new StringBuilder();
    sb.append(listItem(deepness, "Enum", schema.getEnum()));
    sb.append(properties(deepness, "Property", schema.getProperties(), true, context));
    if (schema instanceof ComposedSchema) {
      sb.append(schema(deepness, (ComposedSchema) schema, context));
    } else if (schema instanceof ArraySchema) {
      sb.append(items(deepness, resolve(((ArraySchema) schema).getItems()), context));
    }
    return sb.toString();
  }

  protected String items(int deepness, ChangedSchema schema) {
    StringBuilder sb = new StringBuilder();
    String type = type(schema.getNewSchema());
    if (schema.isChangedType()) {
      type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema());
    }
    sb.append(items(deepness, "Changed items", type, schema.getNewSchema().getDescription()));
    sb.append(schema(deepness, schema));
    return sb.toString();
  }

  protected String items(int deepness, Schema schema, DiffContext context) {
    return items(deepness, "Items", type(schema), schema.getDescription())
        + schema(deepness, schema, context);
  }

  protected String items(int deepness, String title, String type, String description) {
    return format(
        "%s%s (%s):" + "\n%s\n",
        indent(deepness), title, type, metadata(indent(deepness + 1), description));
  }

  protected String properties(
      final int deepness,
      String title,
      Map<String, Schema> properties,
      boolean showContent,
      DiffContext context) {
    StringBuilder sb = new StringBuilder();
    if (properties != null) {
      properties.forEach(
          (key, value) -> {
            sb.append(property(deepness, title, key, resolve(value)));
            if (showContent) {
              sb.append(schema(deepness + 1, resolve(value), context));
            }
          });
    }
    return sb.toString();
  }

  protected String property(int deepness, String name, ChangedSchema schema) {
    StringBuilder sb = new StringBuilder();
    String type = type(schema.getNewSchema());
    if (schema.isChangedType()) {
      type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema());
    }
    sb.append(
        property(deepness, "Changed property", name, type, schema.getNewSchema().getDescription()));
    sb.append(schema(++deepness, schema));
    return sb.toString();
  }

  protected String property(int deepness, String title, String name, Schema schema) {
    return property(deepness, title, name, type(schema), schema.getDescription());
  }

  protected String property(
      int deepness, String title, String name, String type, String description) {
    return format(
        "%s* %s `%s` (%s)\n%s\n",
        indent(deepness), title, name, type, metadata(indent(deepness + 1), description));
  }

  protected String listDiff(int deepness, String name, ChangedList<?> listDiff) {
    if (listDiff == null) {
      return "";
    }
    return listItem(deepness, "Added " + name, listDiff.getIncreased())
        + listItem(deepness, "Removed " + name, listDiff.getMissing());
  }

  protected <T> String listItem(int deepness, String name, List<T> list) {
    StringBuilder sb = new StringBuilder();
    if (list != null && list.size() > 0) {
      sb.append(format("%s%s value%s:\n\n", indent(deepness), name, list.size() > 1 ? "s" : ""));
      list.forEach(p -> sb.append(format("%s* `%s`\n", indent(deepness), p)));
    }
    return sb.toString();
  }

  protected String parameters(ChangedParameters changedParameters) {
    List<ChangedParameter> changed = changedParameters.getChanged();
    StringBuilder sb = new StringBuilder("\n");
    sb.append(listParameter("Added", changedParameters.getIncreased()))
        .append(listParameter("Deleted", changedParameters.getMissing()));
    changed.stream().map(this::itemParameter).forEach(sb::append);
    return sb.toString();
  }

  protected String listParameter(String title, List<Parameter> parameters) {
    StringBuilder sb = new StringBuilder();
    parameters.stream().map(p -> itemParameter(title, p)).forEach(sb::append);
    return sb.toString();
  }

  protected String itemParameter(String title, Parameter parameter) {
    return this.itemParameter(
        title, parameter.getName(), parameter.getIn(), parameter.getDescription());
  }

  protected String itemParameter(String title, String name, String in, String description) {
    return format("%s: ", title)
        + code(name)
        + " in "
        + code(in)
        + '\n'
        + metadata(description)
        + '\n';
  }

  protected String itemParameter(ChangedParameter param) {
    Parameter rightParam = param.getNewParameter();
    if (param.isDeprecated()) {
      return itemParameter(
          "Deprecated", rightParam.getName(), rightParam.getIn(), rightParam.getDescription());
    }
    return itemParameter(
        "Changed", rightParam.getName(), rightParam.getIn(), rightParam.getDescription());
  }

  protected String code(String string) {
    return CODE + string + CODE;
  }

  protected String metadata(String name, ChangedMetadata changedMetadata) {
    return metadata("", name, changedMetadata);
  }

  protected String metadata(String beginning, String name, ChangedMetadata changedMetadata) {
    if (changedMetadata == null) {
      return "";
    }
    if (!isUnchanged(changedMetadata) && showChangedMetadata) {
      return format(
          "Changed %s:\n%s\nto:\n%s\n\n",
          name,
          metadata(beginning, changedMetadata.getLeft()),
          metadata(beginning, changedMetadata.getRight()));
    } else {
      return metadata(beginning, name, changedMetadata.getRight());
    }
  }

  protected String metadata(String metadata) {
    return metadata("", metadata);
  }

  protected String metadata(String beginning, String name, String metadata) {
    if (StringUtils.isBlank(metadata)) {
      return "";
    }
    return blockquote(beginning, metadata);
  }

  protected String metadata(String beginning, String metadata) {
    if (StringUtils.isBlank(metadata)) {
      return "";
    }
    return blockquote(beginning, metadata);
  }

  protected String blockquote(String beginning) {
    return beginning + BLOCKQUOTE;
  }

  protected String blockquote(String beginning, String text) {
    String blockquote = blockquote(beginning);
    return blockquote + text.trim().replaceAll("\n", "\n" + blockquote) + '\n';
  }

  protected String type(Schema schema) {
    String result = "object";
    if (schema instanceof ArraySchema) {
      result = "array";
    } else if (schema.getType() != null) {
      result = schema.getType();
    }
    return result;
  }

  protected String indent(int deepness) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < deepness; i++) {
      sb.append(PRE_LI);
    }
    return sb.toString();
  }

  protected Schema resolve(Schema schema) {
    return refPointer.resolveRef(
        diff.getNewSpecOpenApi().getComponents(), schema, schema.get$ref());
  }
}