package zio.http.endpoint.openapi

import java.util.UUID

import scala.annotation.tailrec
import scala.collection.immutable.ListMap
import scala.collection.{immutable, mutable}

import zio._
import zio.json.ast.Json

import zio.schema.Schema.{Record, Transform}
import zio.schema.codec.JsonCodec
import zio.schema.{Schema, TypeId}

import zio.http._
import zio.http.codec.HttpCodec.Metadata
import zio.http.codec.HttpCodecType.Content
import zio.http.codec._
import zio.http.endpoint._
import zio.http.endpoint.openapi.JsonSchema.{SchemaRef, SchemaSpec, SchemaStyle}
import zio.http.endpoint.openapi.OpenAPI.SecurityScheme.SecurityRequirement
import zio.http.endpoint.openapi.OpenAPI.{Key, Path, PathItem, ReferenceOr, SecurityScheme}
import zio.http.internal.StringSchemaCodec

object OpenAPIGen {
  private val PathWildcard = "pathWildcard"

  private[openapi] def groupMap[A, K, B](chunk: Chunk[A])(key: A => K)(f: A => B): immutable.Map[K, Chunk[B]] = {
    val m      = mutable.Map.empty[K, mutable.Builder[B, Chunk[B]]]
    for (elem <- chunk) {
      val k    = key(elem)
      val bldr = m.getOrElseUpdate(k, Chunk.newBuilder[B])
      bldr += f(elem)
    }
    class Result extends runtime.AbstractFunction1[(K, mutable.Builder[B, Chunk[B]]), Unit] {
      var built = immutable.Map.empty[K, Chunk[B]]

      def apply(kv: (K, mutable.Builder[B, Chunk[B]])): Unit =
        built = built.updated(kv._1, kv._2.result())
    }
    val result = new Result
    m.foreach(result)
    result.built
  }

  final case class MetaCodec[T](codec: T, annotations: Chunk[HttpCodec.Metadata[_]]) {
    lazy val docs: Doc = {
      val annotatedDoc    = annotations.foldLeft(Doc.empty) {
        case (doc, HttpCodec.Metadata.Documented(nextDoc)) => doc + nextDoc
        case (doc, _)                                      => doc
      }
      val trailingPathDoc = codec.asInstanceOf[Any] match {
        case SegmentCodec.Trailing =>
          Doc.p(
            Doc.Span.bold("WARNING: This is wildcard path segment. There is no official OpenAPI support for this."),
          ) +
            Doc.p("Tools might URL encode this segment and it might not work as expected.")
        case _                     =>
          Doc.empty
      }
      annotatedDoc + trailingPathDoc
    }

    lazy val docsOpt: Option[Doc] = if (docs.isEmpty) None else Some(docs)

    lazy val examples: Map[String, Any] = annotations.foldLeft(Map.empty[String, Any]) {
      case (examples, HttpCodec.Metadata.Examples(nextExamples)) => examples ++ nextExamples
      case (examples, _)                                         => examples
    }

    def examples(schema: Schema[_]): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
      examples(schema, generate = false, MediaType.application.json)

    def examples(schema: Schema[_], generate: Boolean): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
      examples(schema, generate, MediaType.application.json)

    private[openapi] def examples(
      schema: Schema[_],
      generate: Boolean,
      mediaType: MediaType,
    ): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
      (if (generate && examples.isEmpty) generateExamples(schema) else examples).map { case (k, v) =>
        k -> OpenAPI.ReferenceOr.Or(OpenAPI.Example(toExampleValue(schema, v, mediaType)))
      }

    def name: Option[String] =
      codec match {
        case value: SegmentCodec[_] =>
          value match {
            case SegmentCodec.BoolSeg(name) => Some(name)
            case SegmentCodec.IntSeg(name)  => Some(name)
            case SegmentCodec.LongSeg(name) => Some(name)
            case SegmentCodec.Text(name)    => Some(name)
            case SegmentCodec.UUID(name)    => Some(name)
            case SegmentCodec.Trailing      => Some(PathWildcard)
            case _                          => None
          }
        case _                      =>
          findName(annotations)
      }

    def required: Boolean =
      !annotations.exists(_.isInstanceOf[HttpCodec.Metadata.Optional[_]])

    def deprecated: Boolean =
      annotations.exists(_.isInstanceOf[HttpCodec.Metadata.Deprecated[_]])
  }
  final case class AtomizedMetaCodecs(
    method: Chunk[MetaCodec[SimpleCodec[Method, _]]],
    path: Chunk[MetaCodec[SegmentCodec[_]]],
    query: Chunk[MetaCodec[HttpCodec.Query[_]]],
    header: Chunk[MetaCodec[HttpCodec.Header[_]]],
    content: Chunk[MetaCodec[HttpCodec.Atom[Content, _]]],
    status: Chunk[MetaCodec[HttpCodec.Status[_]]],
  ) {
    def append(metaCodec: MetaCodec[_]): AtomizedMetaCodecs = metaCodec match {
      case MetaCodec(codec: HttpCodec.Method[_], annotations) =>
        copy(
          method =
            (method :+ MetaCodec(codec.codec, annotations)).asInstanceOf[Chunk[MetaCodec[SimpleCodec[Method, _]]]],
        )
      case MetaCodec(_: SegmentCodec[_], _)                   =>
        copy(path = path :+ metaCodec.asInstanceOf[MetaCodec[SegmentCodec[_]]])
      case MetaCodec(_: HttpCodec.Query[_], _)                =>
        copy(query = query :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Query[_]]])
      case MetaCodec(_: HttpCodec.Header[_], _)               =>
        copy(header = header :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Header[_]]])
      case MetaCodec(_: HttpCodec.Status[_], _)               =>
        copy(status = status :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Status[_]]])
      case MetaCodec(_: HttpCodec.Content[_], _)              =>
        copy(content = content :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Atom[HttpCodecType.Content, _]]])
      case MetaCodec(_: HttpCodec.ContentStream[_], _)        =>
        copy(content = content :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Atom[HttpCodecType.Content, _]]])
      case _                                                  => this
    }

    def ++(that: AtomizedMetaCodecs): AtomizedMetaCodecs =
      AtomizedMetaCodecs(
        method ++ that.method,
        path ++ that.path,
        query ++ that.query,
        header ++ that.header,
        content ++ that.content,
        status ++ that.status,
      )

    def contentExamples: Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
      contentExamples(genExamples = false, MediaType.application.json)

    def contentExamples(genExamples: Boolean): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
      contentExamples(genExamples, MediaType.application.json)

    private[openapi] def contentExamples(
      genExamples: Boolean,
      mediaType: MediaType,
    ): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
      content.flatMap {
        case mc @ MetaCodec(HttpCodec.Content(codec, _, _), _) if codec.lookup(mediaType).isDefined       =>
          mc.examples(codec.lookup(mediaType).get._2.schema, genExamples, mediaType)
        case mc @ MetaCodec(HttpCodec.ContentStream(codec, _, _), _) if codec.lookup(mediaType).isDefined =>
          mc.examples(codec.lookup(mediaType).get._2.schema, genExamples, mediaType)
        case _                                                                                            =>
          Map.empty[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]]
      }.toMap

    // in case of alternatives,
    // the doc to the alternation is added to all sub elements of the alternatives.
    // This is not ideal. But it is the best we can do.
    // To get the doc that is only for the alternation, we take the intersection of all docs,
    // since only the alternation doc is added to all sub elements.
    def contentDocs: Doc =
      content
        .flatMap(_.docsOpt)
        .map(_.flattened)
        .reduceOption(_ intersect _)
        .flatMap(_.reduceOption(_ + _))
        .getOrElse(Doc.empty)

    def optimize: AtomizedMetaCodecs =
      AtomizedMetaCodecs(
        method.materialize,
        path.materialize,
        query.materialize,
        header.materialize,
        content.materialize,
        status.materialize,
      )
  }

  object AtomizedMetaCodecs {
    def empty: AtomizedMetaCodecs = AtomizedMetaCodecs(
      method = Chunk.empty,
      path = Chunk.empty,
      query = Chunk.empty,
      header = Chunk.empty,
      content = Chunk.empty,
      status = Chunk.empty,
    )

    def flatten[R, A](codec: HttpCodec[R, A]): AtomizedMetaCodecs = {
      val atoms = flattenedAtoms(codec)

      val flattened = atoms
        .foldLeft(AtomizedMetaCodecs.empty) { case (acc, atom) =>
          acc.append(atom)
        }
        .optimize
      flattened
    }

    private def flattenedAtoms[R, A](
      in: HttpCodec[R, A],
      annotations: Chunk[HttpCodec.Metadata[Any]] = Chunk.empty,
    ): Chunk[MetaCodec[_]] =
      in match {
        case codec @ HttpCodec.Combine(left, right, combiner) =>
          flattenedAtoms(
            left,
            HttpCodec
              .reduceExamplesLeft[A, codec.Left, codec.Right](
                annotations.asInstanceOf[Chunk[HttpCodec.Metadata[A]]],
                combiner,
              )
              .asInstanceOf[Chunk[HttpCodec.Metadata[Any]]],
          ) ++
            flattenedAtoms(
              right,
              HttpCodec
                .reduceExamplesRight[A, codec.Left, codec.Right](
                  annotations.asInstanceOf[Chunk[HttpCodec.Metadata[A]]],
                  combiner,
                )
                .asInstanceOf[Chunk[HttpCodec.Metadata[Any]]],
            )
        case path: HttpCodec.Path[_]                          => metaCodecFromPathCodec(path.pathCodec, annotations)
        case atom: HttpCodec.Atom[_, A]                       => Chunk(MetaCodec(atom, annotations))
        case map: HttpCodec.TransformOrFail[_, _, _]          =>
          flattenedAtoms(map.api, annotations.map(_.transformOrFail(map.g.asInstanceOf[Any => Either[String, Any]])))
        case HttpCodec.Empty                                  => Chunk.empty
        case HttpCodec.Halt                                   => Chunk.empty
        case _: HttpCodec.Fallback[_, _, _]       => in.alternatives.map(_._1).flatMap(flattenedAtoms(_, annotations))
        case HttpCodec.Annotated(api, annotation) =>
          flattenedAtoms(api, annotations :+ annotation.asInstanceOf[HttpCodec.Metadata[Any]])
      }
  }

  private def generateExamples(schema: Schema[_], name: String = "generated"): Map[String, Any] =
    schema match {
      case collection: Schema.Collection[_, _] =>
        collection match {
          case Schema.Sequence(elementSchema, fromChunk, _, _, _)               =>
            elementSchema.defaultValue.map(v => Map(name -> fromChunk(Chunk.single(v)))).getOrElse(Map.empty)
          case map @ Schema.Map(_, _, _)                                        =>
            val example = for {
              k <- map.keySchema.defaultValue
              v <- map.valueSchema.defaultValue
            } yield map.fromChunk(Chunk.single(k -> v))
            example.map(v => Map(name -> v)).getOrElse(Map.empty)
          case map @ Schema.NonEmptyMap(_, _, _)                                =>
            val example = for {
              k <- map.keySchema.defaultValue
              v <- map.valueSchema.defaultValue
            } yield map.fromChunk(Chunk.single(k -> v))
            example.map(v => Map(name -> v)).getOrElse(Map.empty)
          case Schema.NonEmptySequence(elementSchema, fromChunkOption, _, _, _) =>
            elementSchema.defaultValue.map(v => Map(name -> fromChunkOption(Chunk.single(v)))).getOrElse(Map.empty)
          case set @ Schema.Set(elementSchema, _)                               =>
            elementSchema.defaultValue
              .map(v => set.fromChunk(Chunk.single(v)))
              .map(v => Map(name -> v))
              .getOrElse(Map.empty)

        }
      case Transform(schema, _, _, _, _)       =>
        generateExamples(schema, name)
      case Schema.Optional(schema, _)          =>
        generateExamples(schema, name).map { case (k, v) => k -> Some(v) }
      case Schema.Fail(_, _)                   =>
        Map.empty
      case Schema.Either(left, right, _)       =>
        generateExamples(left, "generatedLeft") ++ generateExamples(right, "generatedRight")
      case Schema.Fallback(left, right, _, _)  =>
        generateExamples(left, "generatedLeft") ++ generateExamples(right, "generatedRight")
      case Schema.Lazy(schema0)                =>
        generateExamples(schema0())
      case Schema.Dynamic(_)                   =>
        Map.empty
      case s @ Schema.Primitive(_, _)          =>
        s.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty)
      case enumSchema: Schema.Enum[_]          =>
        enumSchema.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty)
      case record: Record[_]                   =>
        record.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty)
      case t: Schema.Tuple2[_, _]              =>
        t.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty)
    }

  def method(in: Chunk[MetaCodec[SimpleCodec[Method, _]]]): Method = {
    if (in.size > 1) throw new Exception("Multiple methods not supported")
    in.collectFirst { case MetaCodec(SimpleCodec.Specified(method: Method), _) => method }
      .getOrElse(throw new Exception("No method specified"))
  }

  def metaCodecFromPathCodec(
    codec: PathCodec[_],
    annotations: Chunk[HttpCodec.Metadata[_]],
  ): Chunk[MetaCodec[SegmentCodec[_]]] = {
    def loop(
      path: PathCodec[_],
      annotations: Chunk[HttpCodec.Metadata[_]],
    ): Chunk[(SegmentCodec[_], Chunk[HttpCodec.Metadata[_]])] = path match {
      case PathCodec.Annotated(codec, newAnnotations) =>
        loop(codec, newAnnotations.map(toHttpCodecAnnotations) ++ annotations)
      case PathCodec.Segment(segment)                 => Chunk(segment -> annotations)

      case PathCodec.Concat(left, right, combiner) =>
        loop(left, HttpCodec.reduceExamplesLeft(annotations.asInstanceOf[Chunk[HttpCodec.Metadata[Any]]], combiner)) ++
          loop(right, HttpCodec.reduceExamplesRight(annotations.asInstanceOf[Chunk[HttpCodec.Metadata[Any]]], combiner))

      case codec @ PathCodec.TransformOrFail(api, _, g) =>
        loop(
          api,
          annotations.map(_.transform { v =>
            g(v.asInstanceOf[codec.Out]) match {
              case Left(error)  => throw new Exception(error)
              case Right(value) => value
            }
          }),
        )
      case PathCodec.Fallback(left, _)                  =>
        loop(left, annotations)
    }

    loop(codec, annotations).map { case (sc, annotations) =>
      MetaCodec(sc.asInstanceOf[SegmentCodec[_]], annotations.asInstanceOf[Chunk[HttpCodec.Metadata[Any]]])
    }.asInstanceOf[Chunk[MetaCodec[SegmentCodec[_]]]]
  }

  def toHttpCodecAnnotations(annotation: PathCodec.MetaData[_]): HttpCodec.Metadata[_] =
    annotation match {
      case PathCodec.MetaData.Documented(value)  => HttpCodec.Metadata.Documented(value)
      case PathCodec.MetaData.Examples(examples) => HttpCodec.Metadata.Examples(examples)
    }

  def contentAsJsonSchema[R, A](
    codec: HttpCodec[R, A],
    metadata: Chunk[HttpCodec.Metadata[_]] = Chunk.empty,
    referenceType: SchemaStyle = SchemaStyle.Inline,
    wrapInObject: Boolean = false,
    omitDescription: Boolean = false,
  )(mediaType: MediaType): JsonSchema = {
    val refType             = SchemaRef(SchemaSpec.OpenAPI, referenceType)
    val descriptionFromMeta = if (omitDescription) None else description(metadata)
    codec match {
      case atom: HttpCodec.Atom[_, _]                              =>
        atom match {
          case HttpCodec.Content(codec, maybeName, _) if wrapInObject       =>
            val name =
              findName(metadata).orElse(maybeName).getOrElse(throw new Exception("Multipart content without name"))
            JsonSchema.obj(
              name -> JsonSchema
                .fromZSchema(codec.lookup(mediaType).map(_._2.schema).getOrElse(codec.defaultSchema), refType)
                .description(descriptionFromMeta)
                .deprecated(deprecated(metadata))
                .nullable(optional(metadata)),
            )
          case HttpCodec.ContentStream(codec, maybeName, _)
              if wrapInObject && codec
                .lookup(mediaType)
                .map(_._2.schema)
                .getOrElse(codec.defaultSchema) == Schema[Byte] =>
            val name =
              findName(metadata).orElse(maybeName).getOrElse(throw new Exception("Multipart content without name"))
            JsonSchema.obj(
              name -> JsonSchema
                .fromZSchema(codec.lookup(mediaType).map(_._2.schema).getOrElse(codec.defaultSchema), refType)
                .description(descriptionFromMeta)
                .deprecated(deprecated(metadata))
                .nullable(optional(metadata))
                // currently we have no information about the encoding. So we just assume binary
                .contentEncoding(JsonSchema.ContentEncoding.Binary)
                .contentMediaType(MediaType.application.`octet-stream`.fullType),
            )
          case HttpCodec.ContentStream(codec, maybeName, _) if wrapInObject =>
            val name =
              findName(metadata).orElse(maybeName).getOrElse(throw new Exception("Multipart content without name"))
            JsonSchema.obj(
              name -> JsonSchema
                .ArrayType(
                  Some(
                    JsonSchema
                      .fromZSchema(
                        codec.lookup(mediaType).map(_._2.schema).getOrElse(codec.defaultSchema),
                        refType,
                      ),
                  ),
                  None,
                  uniqueItems = false,
                )
                .description(descriptionFromMeta)
                .deprecated(deprecated(metadata))
                .nullable(optional(metadata)),
            )
          case HttpCodec.Content(codec, _, _)                               =>
            JsonSchema
              .fromZSchema(codec.lookup(mediaType).map(_._2.schema).getOrElse(codec.defaultSchema), refType)
              .description(descriptionFromMeta)
              .deprecated(deprecated(metadata))
              .nullable(optional(metadata))
          case HttpCodec.ContentStream(codec, _, _)                         =>
            val schema = codec.lookup(mediaType).map(_._2.schema).getOrElse(codec.defaultSchema)
            if (schema == Schema[Byte]) {
              JsonSchema
                .fromZSchema(schema, refType)
                .description(descriptionFromMeta)
                .deprecated(deprecated(metadata))
                .nullable(optional(metadata))
                .contentEncoding(JsonSchema.ContentEncoding.Binary)
                .contentMediaType(mediaType.fullType)
            } else {
              JsonSchema
                .ArrayType(
                  Some(JsonSchema.fromZSchema(schema, refType)),
                  None,
                  uniqueItems = false,
                )
                .description(descriptionFromMeta)
                .deprecated(deprecated(metadata))
                .nullable(optional(metadata))
            }
          case _                                                            => JsonSchema.Null
        }
      case HttpCodec.Annotated(codec, data)                        =>
        contentAsJsonSchema(codec, metadata :+ data, referenceType, wrapInObject, omitDescription)(mediaType)
      case HttpCodec.TransformOrFail(api, _, _)                    =>
        contentAsJsonSchema(api, metadata, referenceType, wrapInObject)(mediaType)
      case HttpCodec.Empty                                         => JsonSchema.Null
      case HttpCodec.Halt                                          => JsonSchema.Null
      case HttpCodec.Combine(left, right, _) if isMultipart(codec) =>
        (
          contentAsJsonSchema(left, Chunk.empty, referenceType, wrapInObject = true)(mediaType),
          contentAsJsonSchema(right, Chunk.empty, referenceType, wrapInObject = true)(mediaType),
        ) match {
          case (left, right) =>
            val annotations = left.annotations ++ right.annotations
            (left.withoutAnnotations, right.withoutAnnotations) match {
              case (JsonSchema.Object(p1, _, r1), JsonSchema.Object(p2, _, r2)) =>
                // seems odd to allow additional properties for multipart. So just hardcode it to false
                JsonSchema
                  .Object(p1 ++ p2, Left(false), r1 ++ r2)
                  .deprecated(deprecated(metadata))
                  .nullable(optional(metadata))
                  .description(descriptionFromMeta)
                  .annotate(annotations)
              case (JsonSchema.Object(p, _, r), JsonSchema.Null)                =>
                JsonSchema
                  .Object(p, Left(false), r)
                  .deprecated(deprecated(metadata))
                  .nullable(optional(metadata))
                  .description(descriptionFromMeta)
                  .annotate(annotations)
              case (JsonSchema.Null, JsonSchema.Object(p, _, r))                =>
                JsonSchema
                  .Object(p, Left(false), r)
                  .deprecated(deprecated(metadata))
                  .nullable(optional(metadata))
                  .description(descriptionFromMeta)
                  .annotate(annotations)
              case _ => throw new IllegalArgumentException("Multipart content without name.")
            }

        }
      case HttpCodec.Combine(left, right, _)                       =>
        (
          contentAsJsonSchema(left, Chunk.empty, referenceType, wrapInObject, omitDescription)(mediaType),
          contentAsJsonSchema(right, Chunk.empty, referenceType, wrapInObject, omitDescription)(mediaType),
        ) match {
          case (JsonSchema.Null, JsonSchema.Null) =>
            JsonSchema.Null
          case (JsonSchema.Null, schema)          =>
            schema
              .deprecated(deprecated(metadata))
              .nullable(optional(metadata))
              .description(descriptionFromMeta)
          case (schema, JsonSchema.Null)          =>
            schema
              .deprecated(deprecated(metadata))
              .nullable(optional(metadata))
              .description(descriptionFromMeta)
          case _                                  =>
            throw new IllegalStateException("A non multipart combine, should lead to at least one null schema.")
        }
      case HttpCodec.Fallback(_, _, _, _) => throw new IllegalArgumentException("Fallback not supported at this point")
    }
  }

  private def findName(metadata: Chunk[HttpCodec.Metadata[_]]): Option[String] =
    metadata.reverse
      .find(_.isInstanceOf[Metadata.Named[_]])
      .asInstanceOf[Option[Metadata.Named[Any]]]
      .map(_.name)

  private def description(metadata: Chunk[HttpCodec.Metadata[_]]): Option[String] =
    metadata.collect { case HttpCodec.Metadata.Documented(doc) => doc }
      .reduceOption(_ + _)
      .map(_.toCommonMark)

  private def deprecated(metadata: Chunk[HttpCodec.Metadata[_]]): Boolean =
    metadata.exists(_.isInstanceOf[HttpCodec.Metadata.Deprecated[_]])

  private def optional(metadata: Chunk[HttpCodec.Metadata[_]]): Boolean =
    metadata.exists(_.isInstanceOf[HttpCodec.Metadata.Optional[_]])

  def status[R, A](codec: HttpCodec[R, A]): Option[Status] =
    codec match {
      case HttpCodec.Status(simpleCodec, _) if simpleCodec.isInstanceOf[SimpleCodec.Specified[_]] =>
        Some(simpleCodec.asInstanceOf[SimpleCodec.Specified[Status]].value)
      case HttpCodec.Annotated(codec, _)                                                          =>
        status(codec)
      case HttpCodec.TransformOrFail(api, _, _)                                                   =>
        status(api)
      case HttpCodec.Empty                                                                        =>
        None
      case HttpCodec.Halt                                                                         =>
        None
      case HttpCodec.Combine(left, right, _)                                                      =>
        status(left).orElse(status(right))
      case HttpCodec.Fallback(left, right, _, _)                                                  =>
        status(left).orElse(status(right))
      case _                                                                                      =>
        None
    }

  def isMultipart[R, A](codec: HttpCodec[R, A]): Boolean =
    codec match {
      case HttpCodec.Combine(left, right, _)      =>
        (isContent(left) && isContent(right)) ||
        isMultipart(left) || isMultipart(right)
      case HttpCodec.Annotated(codec, _)          => isMultipart(codec)
      case HttpCodec.TransformOrFail(codec, _, _) => isMultipart(codec)
      case _                                      => false
    }

  def isContent(value: HttpCodec[_, _]): Boolean =
    value match {
      case HttpCodec.Content(_, _, _)             => true
      case HttpCodec.ContentStream(_, _, _)       => true
      case HttpCodec.Annotated(codec, _)          => isContent(codec)
      case HttpCodec.TransformOrFail(codec, _, _) => isContent(codec)
      case HttpCodec.Combine(left, right, _)      => isContent(left) || isContent(right)
      case _                                      => false
    }

  private def toJsonAst(schema: Schema[_], v: Any): Json =
    JsonCodec
      .jsonEncoder(schema.asInstanceOf[Schema[Any]])
      .toJsonAST(v)
      .toOption
      .get

  private def toExampleValue(schema: Schema[_], v: Any, mediaType: MediaType): Json =
    if (mediaType == MediaType.application.json) {
      toJsonAst(schema, v)
    } else {
      // For text/plain and other non-JSON types, use simple string representation
      Json.Str(v.toString)
    }

  def fromEndpoints(
    endpoint1: Endpoint[_, _, _, _, _],
    endpoints: Endpoint[_, _, _, _, _]*,
  ): OpenAPI = fromEndpoints(endpoint1 +: endpoints)

  def fromEndpoints(
    genExamples: Boolean,
    endpoint1: Endpoint[_, _, _, _, _],
    endpoints: Endpoint[_, _, _, _, _]*,
  ): OpenAPI = fromEndpoints(genExamples, endpoint1 +: endpoints)

  def fromEndpoints(
    title: String,
    version: String,
    endpoint1: Endpoint[_, _, _, _, _],
    endpoints: Endpoint[_, _, _, _, _]*,
  ): OpenAPI = fromEndpoints(title, version, endpoint1 +: endpoints)

  def fromEndpoints(
    title: String,
    version: String,
    genExamples: Boolean,
    endpoint1: Endpoint[_, _, _, _, _],
    endpoints: Endpoint[_, _, _, _, _]*,
  ): OpenAPI = fromEndpoints(title, version, genExamples, endpoint1 +: endpoints)

  def fromEndpoints(
    title: String,
    version: String,
    referenceType: SchemaStyle,
    endpoint1: Endpoint[_, _, _, _, _],
    endpoints: Endpoint[_, _, _, _, _]*,
  ): OpenAPI = fromEndpoints(title, version, referenceType, endpoint1 +: endpoints)

  def fromEndpoints(
    title: String,
    version: String,
    referenceType: SchemaStyle,
    genExamples: Boolean,
    endpoint1: Endpoint[_, _, _, _, _],
    endpoints: Endpoint[_, _, _, _, _]*,
  ): OpenAPI = fromEndpoints(title, version, referenceType, genExamples, endpoint1 +: endpoints)

  def fromEndpoints(
    referenceType: SchemaStyle,
    endpoints: Iterable[Endpoint[_, _, _, _, _]],
  ): OpenAPI = if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, referenceType)).reduce(_ ++ _)

  def fromEndpoints(
    referenceType: SchemaStyle,
    genExamples: Boolean,
    endpoints: Iterable[Endpoint[_, _, _, _, _]],
  ): OpenAPI =
    if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, referenceType, genExamples)).reduce(_ ++ _)

  def fromEndpoints(
    endpoints: Iterable[Endpoint[_, _, _, _, _]],
  ): OpenAPI = if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, SchemaStyle.Compact)).reduce(_ ++ _)

  def fromEndpoints(
    genExamples: Boolean,
    endpoints: Iterable[Endpoint[_, _, _, _, _]],
  ): OpenAPI =
    if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, SchemaStyle.Compact, genExamples)).reduce(_ ++ _)

  def fromEndpoints(
    title: String,
    version: String,
    endpoints: Iterable[Endpoint[_, _, _, _, _]],
  ): OpenAPI = fromEndpoints(endpoints).title(title).version(version)

  def fromEndpoints(
    title: String,
    version: String,
    genExamples: Boolean,
    endpoints: Iterable[Endpoint[_, _, _, _, _]],
  ): OpenAPI = fromEndpoints(genExamples, endpoints).title(title).version(version)

  def fromEndpoints(
    title: String,
    version: String,
    referenceType: SchemaStyle,
    endpoints: Iterable[Endpoint[_, _, _, _, _]],
  ): OpenAPI = fromEndpoints(referenceType, endpoints).title(title).version(version)

  def fromEndpoints(
    title: String,
    version: String,
    referenceType: SchemaStyle,
    genExamples: Boolean,
    endpoints: Iterable[Endpoint[_, _, _, _, _]],
  ): OpenAPI = fromEndpoints(referenceType, genExamples, endpoints).title(title).version(version)

  def gen(
    endpoint: Endpoint[_, _, _, _, _],
  ): OpenAPI = gen(endpoint, SchemaStyle.Compact)

  def gen(
    endpoint: Endpoint[_, _, _, _, _],
    referenceType: SchemaStyle,
  ): OpenAPI = gen(endpoint, referenceType, genExamples = false)

  def gen(
    endpoint: Endpoint[_, _, _, _, _],
    referenceStyle: SchemaStyle = SchemaStyle.Compact,
    genExamples: Boolean,
  ): OpenAPI = {
    val referenceType = SchemaRef(SchemaSpec.OpenAPI, referenceStyle)
    val inAtoms       = AtomizedMetaCodecs.flatten(endpoint.input)
    val outs: Map[OpenAPI.StatusOrDefault, Map[MediaType, (JsonSchema, AtomizedMetaCodecs)]] =
      schemaByStatusAndMediaType(
        endpoint.output.alternatives.map(_._1) ++ endpoint.error.alternatives.map(_._1),
        referenceStyle,
        omitContentDescription = true,
      )
    // there is no status for inputs. So we just take the first one (default)
    val ins = schemaByStatusAndMediaType(endpoint.input.alternatives.map(_._1), referenceStyle).values.headOption

    def path: Map[Path, PathItem] = {
      val path           = buildPath(endpoint.input)
      val method0        = method(inAtoms.method)
      // Endpoint has only one doc. But open api has a summery and a description
      val pathItem       = OpenAPI.PathItem.empty
        .copy(description = Some(endpoint.documentation + endpoint.input.doc.getOrElse(Doc.empty)).filter(!_.isEmpty))
      val pathItemWithOp = method0 match {
        case Method.OPTIONS => pathItem.addOptions(operation(endpoint))
        case Method.GET     => pathItem.addGet(operation(endpoint))
        case Method.HEAD    => pathItem.addHead(operation(endpoint))
        case Method.POST    => pathItem.addPost(operation(endpoint))
        case Method.PUT     => pathItem.addPut(operation(endpoint))
        case Method.PATCH   => pathItem.addPatch(operation(endpoint))
        case Method.DELETE  => pathItem.addDelete(operation(endpoint))
        case Method.TRACE   => pathItem.addTrace(operation(endpoint))
        case Method.ANY     => pathItem.any(operation(endpoint))
        case method         => throw new IllegalArgumentException(s"OpenAPI does not support method $method")
      }
      Map(path -> pathItemWithOp)
    }

    def buildPath(in: HttpCodec[_, _]): OpenAPI.Path = {

      def pathCodec(in1: HttpCodec[_, _]): Option[HttpCodec.Path[_]] = in1 match {
        case atom: HttpCodec.Atom[_, _]            =>
          atom match {
            case codec @ HttpCodec.Path(_, _) => Some(codec)
            case _                            => None
          }
        case HttpCodec.Annotated(in, _)            => pathCodec(in)
        case HttpCodec.TransformOrFail(api, _, _)  => pathCodec(api)
        case HttpCodec.Empty                       => None
        case HttpCodec.Halt                        => None
        case HttpCodec.Combine(left, right, _)     => pathCodec(left).orElse(pathCodec(right))
        case HttpCodec.Fallback(left, right, _, _) => pathCodec(left).orElse(pathCodec(right))
      }

      val pathString = {
        val codec = pathCodec(in).getOrElse(throw new Exception("No path found.")).pathCodec
        if (codec.render.endsWith(SegmentCodec.Trailing.render))
          codec.renderIgnoreTrailing + s"{$PathWildcard}"
        else codec.render
      }
      OpenAPI.Path.fromString(pathString).getOrElse(throw new Exception(s"Invalid path: $pathString"))
    }

    def getSecurity(
      endpoint: Endpoint[_, _, _, _, _],
    ): List[SecurityRequirement] =
      endpointSecurity(endpoint)

    def customAuthSchemes(codec: HttpCodec[_, _]): List[(String, SecurityScheme.ApiKey)] = {
      val atoms         = AtomizedMetaCodecs.flatten(codec)
      val headerSchemes = atoms.header.map { meta =>
        val headerAtom = meta.codec.asInstanceOf[HttpCodec.Header[_]]
        val name       = headerAtom.headerType.names.head
        val in         =
          if (name.equalsIgnoreCase("cookie")) SecurityScheme.ApiKey.In.Cookie
          else SecurityScheme.ApiKey.In.Header
        (name, SecurityScheme.ApiKey(description = None, name = name, in = in))
      }.toList
      val querySchemes  = atoms.query.map { meta =>
        val queryAtom = meta.codec.asInstanceOf[HttpCodec.Query[_]]
        val name      = queryAtom.codec.recordFields.head._1.fieldName
        (name, SecurityScheme.ApiKey(description = None, name = name, in = SecurityScheme.ApiKey.In.Query))
      }.toList
      headerSchemes ++ querySchemes
    }

    def endpointSecurity(endpoint: Endpoint[_, _, _, _, _]): List[SecurityRequirement] = {
      def loop(authType: AuthType): List[SecurityRequirement] = authType match {
        case AuthType.ScopedAuth(auth, scopes)                  =>
          loop(auth).map { req =>
            SecurityRequirement(req.securitySchemes.map { case (k, _) => k -> scopes })
          }
        case AuthType.Basic | AuthType.Bearer | AuthType.Digest =>
          List(SecurityRequirement(Map(authType.toString() -> Nil)))
        case custom: AuthType.Custom[_]                         =>
          val schemes = customAuthSchemes(custom.codec)
          List(SecurityRequirement(schemes.map { case (name, _) => name -> Nil }.toMap))
        case or: AuthType.Or[_, _, _]                           =>
          loop(or.auth1) ++ loop(or.auth2)
        case AuthType.WithStatus(auth, _)                       =>
          loop(auth)
        case _                                                  => Nil
      }
      loop(endpoint.authType.asInstanceOf[AuthType])
    }

    def authResponse(endpoint: Endpoint[_, _, _, _, _]): OpenAPI.Responses = {
      val authType                      = endpoint.authType.asInstanceOf[AuthType]
      def isNone(at: AuthType): Boolean = at match {
        case AuthType.None             => true
        case AuthType.WithStatus(a, _) => isNone(a)
        case AuthType.ScopedAuth(a, _) => isNone(a)
        case _                         => false
      }
      if (isNone(authType)) Map.empty
      else {
        val status      = authType.unauthorizedStatus
        val statusKey   = OpenAPI.StatusOrDefault.StatusValue(status)
        val description = status match {
          case Status.Unauthorized => "Unauthorized"
          case Status.NotFound     => "Not Found"
          case Status.Forbidden    => "Forbidden"
          case Status.BadRequest   => "Bad Request"
          case other               => other.text
        }
        Map(statusKey -> OpenAPI.ReferenceOr.Or(OpenAPI.Response(description = Some(Doc.p(description)))))
      }
    }

    def operation(endpoint: Endpoint[_, _, _, _, _]): OpenAPI.Operation = {
      val maybeDoc = Some(endpoint.documentation + pathDoc).filter(!_.isEmpty)
      OpenAPI.Operation(
        tags = endpoint.tags,
        summary = None,
        description = maybeDoc,
        externalDocs = None,
        operationId = None,
        parameters = parameters,
        requestBody = requestBody,
        responses = authResponse(endpoint) ++ responses,
        callbacks = Map.empty,
        security = getSecurity(endpoint),
        servers = Nil,
      )
    }

    def pathDoc: Doc =
      inAtoms.path
        .flatMap(_.docsOpt)
        .map(_.flattened)
        .reduceOption(_ intersect _)
        .flatMap(_.reduceOption(_ + _))
        .getOrElse(Doc.empty)

    def requestBody: Option[OpenAPI.ReferenceOr[OpenAPI.RequestBody]] =
      ins.map { mediaTypes =>
        val combinedAtomizedCodecs = mediaTypes.map { case (_, (_, atomized)) => atomized }
          .reduceOption(_ ++ _)
          .getOrElse(AtomizedMetaCodecs.empty)

        val mediaTypeResponses = mediaTypes.map { case (mediaType, (schema, atomized)) =>
          mediaType.fullType -> OpenAPI.MediaType(
            schema = OpenAPI.ReferenceOr.Or(schema),
            examples = atomized.contentExamples(genExamples, mediaType),
            encoding = Map.empty,
          )
        }
        OpenAPI.ReferenceOr.Or(
          OpenAPI.RequestBody(
            content = mediaTypeResponses,
            required = combinedAtomizedCodecs.content.exists(_.required),
          ),
        )
      }.filter(_.value.content.exists {
        case (_, OpenAPI.MediaType(OpenAPI.ReferenceOr.Or(schema), _, _)) =>
          schema.withoutAnnotations != JsonSchema.Null
        case _                                                            => true
      })

    def responses: OpenAPI.Responses =
      responsesForAlternatives(outs, genExamples)

    def parameters: Set[OpenAPI.ReferenceOr[OpenAPI.Parameter]] =
      queryParams ++ pathParams ++ headerParams

    def queryParams: Set[OpenAPI.ReferenceOr[OpenAPI.Parameter]] =
      inAtoms.query.collect { case mc @ MetaCodec(HttpCodec.Query(codec, _), _) =>
        val recordSchema = (codec.schema match {
          case schema if schema.isInstanceOf[Schema.Optional[_]] => schema.asInstanceOf[Schema.Optional[_]].schema
          case _                                                 => codec.schema
        }).asInstanceOf[Schema.Record[Any]]
        val examples     = mc.examples.map { case (exName, ex) =>
          exName -> recordSchema.deconstruct(ex)(Unsafe.unsafe)
        }
        codec.recordFields.zipWithIndex.map { case ((field, codec), index) =>
          val docs = codec.schema.annotations.collectFirst { case desc: zio.schema.annotation.description =>
            mc.docs + Doc.p(desc.text)
          }.orElse(mc.docsOpt)
          OpenAPI.ReferenceOr.Or(
            OpenAPI.Parameter.queryParameter(
              name = field.name,
              description = docs,
              schema = Some(
                OpenAPI.ReferenceOr.Or(
                  JsonSchema.fromZSchema(codec.schema, SchemaRef(SchemaSpec.OpenAPI, SchemaStyle.Inline)),
                ),
              ),
              deprecated = mc.deprecated,
              style = OpenAPI.Parameter.Style.Form,
              explode = false,
              allowReserved = false,
              examples = examples.map { case (exName, values) =>
                val fieldValue = values(index)
                  .orElse(field.defaultValue)
                  .getOrElse(
                    throw new Exception(s"No value or default value found for field ${exName}_${field.name}"),
                  )
                s"${exName}_${field.name}" -> OpenAPI.ReferenceOr.Or(
                  OpenAPI.Example(value = Json.Str(codec.encode(fieldValue))),
                )
              },
              required = mc.required && !StringSchemaCodec.isOptional(field.schema),
            ),
          )

        }
      }.flatten.toSet

    def pathParams: Set[OpenAPI.ReferenceOr[OpenAPI.Parameter]] =
      inAtoms.path.collect {
        case mc @ MetaCodec(codec, _) if codec != SegmentCodec.Empty && !codec.isInstanceOf[SegmentCodec.Literal] =>
          OpenAPI.ReferenceOr.Or(
            OpenAPI.Parameter.pathParameter(
              name = mc.name.getOrElse(throw new Exception("Path parameter must have a name")),
              description = mc.docsOpt.flatMap(_.flattened.filterNot(_ == pathDoc).reduceOption(_ + _)),
              definition = Some(OpenAPI.ReferenceOr.Or(JsonSchema.fromSegmentCodec(codec))),
              deprecated = mc.deprecated,
              style = OpenAPI.Parameter.Style.Simple,
              examples = mc.examples.map { case (name, value) =>
                name -> OpenAPI.ReferenceOr.Or(OpenAPI.Example(segmentToJson(codec, value)))
              },
            ),
          )
      }.toSet

    def headerParams: Set[OpenAPI.ReferenceOr[OpenAPI.Parameter]] =
      inAtoms.header
        .asInstanceOf[Chunk[MetaCodec[HttpCodec.Header[Any]]]]
        .map { case mc @ MetaCodec(codec, _) =>
          OpenAPI.ReferenceOr.Or(
            OpenAPI.Parameter.headerParameter(
              name = mc.name.getOrElse(codec.headerType.names.head),
              description = mc.docsOpt,
              definition = Some(OpenAPI.ReferenceOr.Or(JsonSchema.String().nullable(!mc.required))),
              deprecated = mc.deprecated,
              examples = mc.examples.map { case (name, value) =>
                // The value should be a Header instance, so we can call renderedValue on it
                val renderedValue = value match {
                  case header: Header => header.renderedValue
                  case other          => other.toString
                }
                name -> OpenAPI.ReferenceOr.Or(OpenAPI.Example(Json.Str(renderedValue)))
              },
              required = mc.required,
            ),
          )
        }
        .toSet

    def genDiscriminator(schema: Schema[_]): Option[OpenAPI.Discriminator] = {
      schema match {
        case enumSchema: Schema.Enum[_] =>
          val noDiscriminator = enumSchema.annotations.contains(zio.schema.annotation.noDiscriminator())
          if (noDiscriminator) None
          else {
            val discriminatorName =
              enumSchema.annotations.collectFirst { case zio.schema.annotation.discriminatorName(name) => name }
            discriminatorName.map { name =>
              val typeMapping = enumSchema.cases.map { case_ =>
                val caseName =
                  case_.annotations.collectFirst { case zio.schema.annotation.caseName(name) => name }
                    .getOrElse(case_.id)
                // There should be no enums with cases that are not records with a nominal id
                // TODO: not true. Since one could build a schema with a enum with a case that is a primitive
                val typeId   =
                  (case_.schema match {
                    case lzy: Schema.Lazy[_]                  => lzy.schema
                    case transform: Schema.Transform[_, _, _] => transform.schema
                    case _                                    => case_.schema
                  })
                    .asInstanceOf[Schema.Record[_]]
                    .id
                    .asInstanceOf[TypeId.Nominal]
                caseName -> schemaReferencePath(typeId, referenceType)
              }
              OpenAPI.Discriminator(name, typeMapping.toMap)
            }
          }

        case _ => None
      }
    }

    def segmentToJson(codec: SegmentCodec[_], value: Any): Json = {
      codec match {
        case SegmentCodec.Empty             => throw new Exception("Empty segment not allowed")
        case SegmentCodec.Literal(_)        => throw new Exception("Literal segment not allowed")
        case SegmentCodec.BoolSeg(_)        => Json.Bool(value.asInstanceOf[Boolean])
        case SegmentCodec.IntSeg(_)         => Json.Num(value.asInstanceOf[Int])
        case SegmentCodec.LongSeg(_)        => Json.Num(value.asInstanceOf[Long])
        case SegmentCodec.Text(_)           => Json.Str(value.asInstanceOf[String])
        case SegmentCodec.UUID(_)           => Json.Str(value.asInstanceOf[UUID].toString)
        case SegmentCodec.Trailing          => throw new Exception("Trailing segment not allowed")
        case SegmentCodec.Combined(_, _, _) => throw new Exception("Combined segment not allowed")
      }
    }

    def jsonSchemaFromCodec(codec: HttpContentCodec[_]): Option[Schema[_]] =
      codec.lookup(MediaType.application.json).map(_._2.schema)

    def componentSchemas: Map[OpenAPI.Key, OpenAPI.ReferenceOr[JsonSchema]] =
      (endpoint.input.alternatives.map(_._1).map(AtomizedMetaCodecs.flatten(_)).flatMap(_.content)
        ++ endpoint.error.alternatives.map(_._1).map(AtomizedMetaCodecs.flatten(_)).flatMap(_.content)
        ++ endpoint.output.alternatives.map(_._1).map(AtomizedMetaCodecs.flatten(_)).flatMap(_.content)).collect {
        case MetaCodec(HttpCodec.Content(codec, _, _), _)
            if jsonSchemaFromCodec(codec).isDefined &&
              nominal(jsonSchemaFromCodec(codec).get, referenceStyle).isDefined =>
          val schemas = JsonSchema.fromZSchemaMultiple(jsonSchemaFromCodec(codec).get, referenceType)
          schemas.children.map { case (key, schema) =>
            OpenAPI.Key.fromString(key.replace(referenceType.spec.path, "")).get -> OpenAPI.ReferenceOr.Or(schema)
          } + (OpenAPI.Key.fromString(nominal(jsonSchemaFromCodec(codec).get, referenceStyle).get).get ->
            OpenAPI.ReferenceOr.Or(schemas.root.discriminator(genDiscriminator(jsonSchemaFromCodec(codec).get))))

        case MetaCodec(HttpCodec.Content(setCodec, _, _), _)
            if jsonSchemaFromCodec(setCodec).isDefined && jsonSchemaFromCodec(setCodec).get.isInstanceOf[Schema.Set[_]]
              && nominal(
                jsonSchemaFromCodec(setCodec).get.asInstanceOf[Schema.Set[_]].elementSchema,
                referenceStyle,
              ).isDefined =>
          val schema  = jsonSchemaFromCodec(setCodec).get.asInstanceOf[Schema.Set[_]].elementSchema
          val schemas = JsonSchema.fromZSchemaMultiple(schema, referenceType)
          schemas.children.map { case (key, schema) =>
            OpenAPI.Key.fromString(key.replace(referenceType.spec.path, "")).get -> OpenAPI.ReferenceOr.Or(schema)
          } + (OpenAPI.Key.fromString(nominal(schema, referenceStyle).get).get ->
            OpenAPI.ReferenceOr.Or(schemas.root.discriminator(genDiscriminator(schema))))

        case MetaCodec(HttpCodec.Content(seqCodec, _, _), _)
            if jsonSchemaFromCodec(seqCodec).isDefined && jsonSchemaFromCodec(seqCodec).get
              .isInstanceOf[Schema.Sequence[_, _, _]]
              && nominal(
                jsonSchemaFromCodec(seqCodec).get.asInstanceOf[Schema.Sequence[_, _, _]].elementSchema,
                referenceStyle,
              ).isDefined =>
          val schema  = jsonSchemaFromCodec(seqCodec).get.asInstanceOf[Schema.Sequence[_, _, _]].elementSchema
          val schemas = JsonSchema.fromZSchemaMultiple(schema, referenceType)
          schemas.children.map { case (key, schema) =>
            OpenAPI.Key.fromString(key.replace(referenceType.spec.path, "")).get -> OpenAPI.ReferenceOr.Or(schema)
          } + (OpenAPI.Key.fromString(nominal(schema, referenceStyle).get).get ->
            OpenAPI.ReferenceOr.Or(schemas.root.discriminator(genDiscriminator(schema))))

        case MetaCodec(HttpCodec.Content(mapCodec, _, _), _)
            if jsonSchemaFromCodec(mapCodec).isDefined && jsonSchemaFromCodec(mapCodec).get
              .isInstanceOf[Schema.Map[_, _]]
              && nominal(
                jsonSchemaFromCodec(mapCodec).get.asInstanceOf[Schema.Map[_, _]].valueSchema,
                referenceStyle,
              ).isDefined =>
          val schema  = jsonSchemaFromCodec(mapCodec).get.asInstanceOf[Schema.Map[_, _]].valueSchema
          val schemas = JsonSchema.fromZSchemaMultiple(schema, referenceType)
          schemas.children.map { case (key, schema) =>
            OpenAPI.Key.fromString(key.replace(referenceType.spec.path, "")).get -> OpenAPI.ReferenceOr.Or(schema)
          } + (OpenAPI.Key.fromString(nominal(schema, referenceStyle).get).get ->
            OpenAPI.ReferenceOr.Or(schemas.root.discriminator(genDiscriminator(schema))))

        case MetaCodec(HttpCodec.ContentStream(codec, _, _), _)
            if jsonSchemaFromCodec(codec).isDefined && nominal(
              jsonSchemaFromCodec(codec).get,
              referenceStyle,
            ).isDefined =>
          val schemas = JsonSchema.fromZSchemaMultiple(jsonSchemaFromCodec(codec).get, referenceType)
          schemas.children.map { case (key, schema) =>
            OpenAPI.Key.fromString(key.replace(referenceType.spec.path, "")).get -> OpenAPI.ReferenceOr.Or(schema)
          } + (OpenAPI.Key.fromString(nominal(jsonSchemaFromCodec(codec).get, referenceStyle).get).get ->
            OpenAPI.ReferenceOr.Or(schemas.root.discriminator(genDiscriminator(jsonSchemaFromCodec(codec).get))))
      }.flatten.toMap

    def httpSecuritySchemes(
      endpoint: Endpoint[_, _, _, _, _],
      params: Set[OpenAPI.ReferenceOr[OpenAPI.Parameter]],
    ): ListMap[Key, ReferenceOr[SecurityScheme]] = {
      def loop(authType: AuthType): ListMap[Key, ReferenceOr[SecurityScheme]] =
        authType match {
          case AuthType.Basic | AuthType.Bearer | AuthType.Digest =>
            ListMap(
              OpenAPI.Key.fromString(authType.toString()).get ->
                ReferenceOr.Or[SecurityScheme.Http](
                  SecurityScheme.Http(
                    scheme = authType.toString(),
                    bearerFormat = None,
                    description = None,
                  ),
                ),
            )
          case AuthType.ScopedAuth(auth, _)                       =>
            loop(auth)
          case custom: AuthType.Custom[_]                         =>
            ListMap(customAuthSchemes(custom.codec).map { case (name, scheme) =>
              OpenAPI.Key.fromString(name).get -> ReferenceOr.Or(scheme)
            }: _*)
          case or: AuthType.Or[_, _, _]                           =>
            loop(or.auth1) ++ loop(or.auth2)
          case AuthType.WithStatus(auth, _)                       =>
            loop(auth)
          case _                                                  => ListMap.empty
        }
      loop(endpoint.authType.asInstanceOf[AuthType])
    }

    def getSecuritySchemes(
      endpoint: Endpoint[_, _, _, _, _],
      params: Set[ReferenceOr[OpenAPI.Parameter]],
    ): ListMap[Key, ReferenceOr[SecurityScheme]] =
      httpSecuritySchemes(endpoint, params)

    def components = OpenAPI.Components(
      schemas = ListMap(componentSchemas.toSeq.sortBy(_._1.name): _*),
      responses = ListMap.empty,
      parameters = ListMap.empty,
      examples = ListMap.empty,
      requestBodies = ListMap.empty,
      headers = ListMap.empty,
      securitySchemes = getSecuritySchemes(endpoint, headerParams ++ queryParams),
      links = ListMap.empty,
      callbacks = ListMap.empty,
    )

    OpenAPI(
      "3.1.0",
      info = OpenAPI.Info(
        title = "",
        description = None,
        termsOfService = None,
        contact = None,
        license = None,
        version = "",
      ),
      servers = Nil,
      paths = ListMap(path.toSeq.sortBy(_._1.name): _*),
      components = Some(components),
      security = getSecurity(endpoint),
      tags = Nil,
      externalDocs = None,
    )
  }

  private def schemaByStatusAndMediaType(
    alternatives: Chunk[HttpCodec[_, _]],
    referenceType: SchemaStyle,
    omitContentDescription: Boolean = false,
  ): Map[OpenAPI.StatusOrDefault, Map[MediaType, (JsonSchema, AtomizedMetaCodecs)]] = {
    val statusAndCodec =
      alternatives.map { codec =>
        val statusOrDefault =
          status(codec).map(OpenAPI.StatusOrDefault.StatusValue(_)).getOrElse(OpenAPI.StatusOrDefault.Default)
        (
          statusOrDefault,
          (
            AtomizedMetaCodecs.flatten(codec),
            contentAsJsonSchema(codec, referenceType = referenceType, omitDescription = omitContentDescription) _,
          ),
        )
      }

    groupMap(statusAndCodec) { case (status, _) => status } { case (_, atomizedAndSchema) =>
      atomizedAndSchema
    }.map { case (status, values) =>
      val mapped = values.filter { case (atomized, _) => atomized.content.nonEmpty }
        .foldLeft(Chunk.empty[(MediaType, (AtomizedMetaCodecs, JsonSchema))]) { case (acc, (atomized, schema)) =>
          if (atomized.content.size > 1) {
            acc :+ ((MediaType.multipart.`form-data`, (atomized, schema(MediaType.multipart.`form-data`))))
          } else {
            val mediaType = atomized.content.headOption match {
              case Some(MetaCodec(HttpCodec.Content(codec, _, _), _))       =>
                codec.defaultMediaType
              case Some(MetaCodec(HttpCodec.ContentStream(codec, _, _), _)) =>
                if (codec.defaultSchema == Schema[Byte]) MediaType.application.`octet-stream`
                else codec.defaultMediaType
              case _                                                        =>
                MediaType.application.`json`
            }
            acc :+ ((mediaType, (atomized, schema(mediaType))))
          }
        }
      status -> groupMap(mapped) { case (mediaType, _) => mediaType } { case (_, atomizedAndSchema) =>
        atomizedAndSchema
      }.map {
        case (mediaType, Chunk((atomized, schema))) if mapped.size == 1 =>
          (mediaType, (schema, atomized))
        case (mediaType, values)                                        =>
          val combinedAtomized: AtomizedMetaCodecs = values
            .map(_._1)
            .reduceOption(_ ++ _)
            .getOrElse(AtomizedMetaCodecs.empty)
          val combinedContentDoc                   = combinedAtomized.contentDocs.toCommonMark
          val vals                                 =
            if (values.forall(v => v._2.isNullable || v._2 == JsonSchema.Null))
              values.map(_._2).filter(_ != JsonSchema.Null)
            else values.map(_._2)
          val alternativesSchema                   = {
            JsonSchema
              .AnyOfSchema(vals.map { schema =>
                schema.description match {
                  case Some(value) => schema.description(value.replace(combinedContentDoc, ""))
                  case None        => schema
                }
              })
              .minify
              .description(combinedContentDoc)
          }
          (mediaType, (alternativesSchema, combinedAtomized))
      }
    }
  }

  @tailrec
  def nominal(schema: Schema[_], referenceType: SchemaStyle): Option[String] =
    schema match {
      case enumSchema: Schema.Enum[_] =>
        enumSchema.id match {
          case TypeId.Structural                                               =>
            None
          case nominal: TypeId.Nominal if referenceType == SchemaStyle.Compact =>
            Some(nominal.typeName)
          case nominal: TypeId.Nominal                                         =>
            Some(nominal.fullyQualified.replace(".", "_"))
        }
      case record: Record[_]          =>
        record.id match {
          case TypeId.Structural                                               =>
            None
          case nominal: TypeId.Nominal if referenceType == SchemaStyle.Compact =>
            Some(nominal.typeName)
          case nominal: TypeId.Nominal                                         =>
            Some(nominal.fullyQualified.replace(".", "_"))
        }
      case t: Transform[_, _, _]      =>
        nominal(t.schema, referenceType)
      case Schema.Optional(inner, _)  =>
        nominal(inner, referenceType)
      case Schema.Lazy(schema0)       =>
        nominal(schema0(), referenceType)
      case _                          => None
    }

  private def responsesForAlternatives(
    codecs: Map[OpenAPI.StatusOrDefault, Map[MediaType, (JsonSchema, AtomizedMetaCodecs)]],
    genExamples: Boolean,
  ): Map[OpenAPI.StatusOrDefault, OpenAPI.ReferenceOr[OpenAPI.Response]] =
    codecs.map { case (status, mediaTypes) =>
      val combinedAtomizedCodecs =
        mediaTypes.map { case (_, (_, atomized)) => atomized }.foldLeft(AtomizedMetaCodecs.empty)(_ ++ _)
      val mediaTypeResponses     = mediaTypes.map { case (mediaType, (schema, atomized)) =>
        mediaType.fullType -> OpenAPI.MediaType(
          schema = OpenAPI.ReferenceOr.Or(schema),
          examples = atomized.contentExamples(genExamples, mediaType),
          encoding = Map.empty,
        )
      }
      status -> OpenAPI.ReferenceOr.Or(
        OpenAPI.Response(
          description = combinedAtomizedCodecs.status.headOption.flatMap(_.docsOpt),
          headers = headersFrom(combinedAtomizedCodecs),
          content = mediaTypeResponses,
          links = Map.empty,
        ),
      )
    }

  private def headersFrom(codec: AtomizedMetaCodecs)                                         = {
    codec.header.map { case mc @ MetaCodec(codec, _) =>
      // todo use all headers
      codec.headerType.names.head -> OpenAPI.ReferenceOr.Or(
        OpenAPI.Header(
          description = mc.docsOpt,
          required = true,
          deprecated = mc.deprecated,
          allowEmptyValue = false,
          schema = Some(JsonSchema.String().nullable(!mc.required)),
        ),
      )
    }.toMap
  }
  private def schemaReferencePath(nominal: TypeId.Nominal, referenceType: SchemaRef): String = {
    referenceType.style match {
      case SchemaStyle.Compact => s"${referenceType.spec.path}${nominal.typeName}"
      case _                   => s"${referenceType.spec.path}${nominal.fullyQualified.replace(".", "_")}}"
    }
  }
}
