/*
 * Copyright 2016 Dennis Vriend
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.dnvriend.component.webservices.postcode

import akka.NotUsed
import akka.actor.ActorSystem
import akka.event.LoggingAdapter
import akka.http.scaladsl.model.{ HttpRequest, HttpResponse }
import akka.stream.Materializer
import akka.stream.scaladsl.Flow
import com.github.dnvriend.component.webservices.generic.HttpClient
import spray.json.DefaultJsonProtocol

import scala.concurrent.{ ExecutionContext, Future }
import scala.util.Try
import scala.util.matching.Regex

case class Address(
  street: String,
  houseNumber: Int,
  houseNumberAddition: String,
  postcode: String,
  city: String,
  municipality: String,
  province: String,
  rdX: Option[Int],
  rdY: Option[Int],
  latitude: Double,
  longitude: Double,
  bagNumberDesignationId: String,
  bagAddressableObjectId: String,
  addressType: String,
  purposes: Option[List[String]],
  surfaceArea: Int,
  houseNumberAdditions: List[String]
)

trait Marshallers extends DefaultJsonProtocol {
  implicit val addressJsonFormat = jsonFormat17(Address)
}

case class GetAddressRequest(zip: String, houseNumber: String)

trait PostcodeClient {
  def address(postcode: String, houseNumber: Int): Future[Option[Address]]

  def address[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(GetAddressRequest, T), (Option[Address], T), NotUsed]
}

object PostcodeClient {
  import spray.json._
  val ZipcodeWithoutSpacePattern: Regex = """([1-9][0-9]{3})([A-Za-z]{2})""".r
  val ZipcodeWithSpacePattern: Regex = """([1-9][0-9]{3})[\s]([A-Za-z]{2})""".r

  def mapToAddress(json: String)(implicit reader: JsonReader[Address]): Option[Address] =
    Try(json.parseJson.convertTo[Address]).toOption

  def responseToString(resp: HttpResponse)(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Future[String] =
    HttpClient.responseToString(resp)

  def getAddressRequestFlow[T]: Flow[(GetAddressRequest, T), (HttpRequest, T), NotUsed] =
    Flow[(GetAddressRequest, T)].map { case (request, id) => (HttpClient.mkGetRequest(s"/rest/addresses/${request.zip}/${request.houseNumber}/"), id) }

  def mapResponseToAddressFlow[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, reader: JsonReader[Address]): Flow[(Try[HttpResponse], T), (Option[Address], T), NotUsed] =
    HttpClient.responseToString[T].map { case (json, id) => (mapToAddress(json), id) }
  /**
   * Returns an option of the zipcode without spaces, if there were any. Any invalid zipcode
   * will be returned as None
   *
   * @param zipcode
   * @return
   */
  def normalizeZipcode(zipcode: String): Option[String] = zipcode.toUpperCase match {
    case ZipcodeWithoutSpacePattern(numbers, letters) => Option(s"$numbers$letters")
    case ZipcodeWithSpacePattern(numbers, letters)    => Option(s"$numbers$letters")
    case _                                            => None
  }

  def apply()(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext, log: LoggingAdapter) = new PostcodeClientImpl
}

class PostcodeClientImpl()(implicit val system: ActorSystem, val mat: Materializer, val ec: ExecutionContext, val log: LoggingAdapter) extends PostcodeClient with Marshallers {
  import PostcodeClient._
  private val client = HttpClient("postcode")

  override def address(postcode: String, houseNumber: Int): Future[Option[Address]] =
    normalizeZipcode(postcode) match {
      case Some(zip) => client.get(s"/rest/addresses/$zip/$houseNumber/")
        .flatMap(responseToString).map(mapToAddress)
      case None => Future.successful(None)
    }

  override def address[T](implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext): Flow[(GetAddressRequest, T), (Option[Address], T), NotUsed] =
    getAddressRequestFlow[T]
      .via(client.cachedHostConnectionFlow[T])
      .via(mapResponseToAddressFlow[T])
}