// Copyright (c) 2018-2020 by Rob Norris
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package skunk.net.protocol

import cats.effect.Resource
import cats.implicits._
import cats.MonadError
import skunk.exception.PostgresErrorException
import skunk.net.message.{ Parse => ParseMessage, Close => _, _ }
import skunk.net.MessageSocket
import skunk.net.Protocol.StatementId
import skunk.Statement
import skunk.util.Namer
import skunk.util.Typer
import skunk.exception.UnknownTypeException
import natchez.Trace

trait Parse[F[_]] {
  def apply[A](statement: Statement[A], ty: Typer): Resource[F, StatementId]
}

object Parse {

  def apply[F[_]: MonadError[?[_], Throwable]: Exchange: MessageSocket: Namer: Trace]: Parse[F] =
    new Parse[F] {

      override def apply[A](statement: Statement[A], ty: Typer): Resource[F, StatementId] =
        statement.encoder.oids(ty) match {

          case Right(os) =>
            Resource.make {
              exchange("parse") {
                for {
                  id <- nextName("statement").map(StatementId)
                  _  <- Trace[F].put(
                          "statement-name"            -> id.value,
                          "statement-sql"             -> statement.sql,
                          "statement-parameter-types" -> os.map(n => ty.typeForOid(n, -1).getOrElse(n)).mkString("[", ", ", "]")
                        )
                  _  <- send(ParseMessage(id.value, statement.sql, os))
                  _  <- send(Flush)
                  _  <- flatExpect {
                          case ParseComplete       => ().pure[F]
                          case ErrorResponse(info) => syncAndFail(statement, info)
                        }
                } yield id
              }
            } { Close[F].apply }

          case Left(err) =>
            Resource.liftF(UnknownTypeException(statement, err).raiseError[F, StatementId])

        }

      def syncAndFail(statement: Statement[_], info: Map[Char, String]): F[Unit] =
        for {
          hi <- history(Int.MaxValue)
          _  <- send(Sync)
          _  <- expect { case ReadyForQuery(_) => }
          a  <- new PostgresErrorException(
                  sql       = statement.sql,
                  sqlOrigin = Some(statement.origin),
                  info      = info,
                  history   = hi,
                ).raiseError[F, Unit]
        } yield a

    }

}