package ru.yandex.atom.service

import akka.actor.{Props, ActorRef, Actor}
import java.net.{MalformedURLException, URL}
import ru.yandex.atom.error.{InternalProblem, UserProblem}
import scala.util.{Failure, Success, Try}
import ru.yandex.wmtools.common.util.URLUtil
import scala.collection.immutable.Queue
import scala.concurrent.duration.{FiniteDuration, Deadline}
import ru.yandex.atom.data.{NormalizedUrl, GeminiRequestType}
import com.typesafe.config.Config
import ru.yandex.atom.utils.actor.AtomActorLogging

/**
 * @author avhaliullin
 */
trait UrlNormalizerActorComponentImpl extends UrlNormalizerActorComponent {
  component: GeminiActorComponent =>

  class UrlNormalizer(config: UrlNormalizerConfig) extends Actor with AtomActorLogging {

    import UrlNormalizerRequest._
    import UrlNormalizerResponse._

    implicit val ec = context.dispatcher

    case object PING

    case class RequestInfo(req: UrlNormalizerRequest.Normalize, client: ActorRef, urls: Set[UrlParts])

    case class UrlParts(host: String, rel: String) {
      lazy val url = {
        val slashes = (if (host.endsWith("/")) 1 else 0) + (if (rel.startsWith("/")) 1 else 0)
        if (slashes > 1) host + rel.substring(1) else if (slashes == 1) host + rel else host + "/" + rel
      }
    }

    object UrlParts {
      def apply(urlString: String): Try[UrlParts] = Try {
        val url = new URL(urlString)
        new UrlParts(URLUtil.getHostName(url, true), URLUtil.getRelativeUrl(url))
      }
    }

    var waitMap = Map[GeminiRequest, RequestInfo]()
    var waitQueue = Queue[(Deadline, GeminiRequest)]()

    def receive = {
      case req@Normalize(id, urls) =>
        val (succeeded, failed) = urls.foldLeft((Set[UrlParts](), Set[String]())) {
          (acc, it) =>
            val (succeeded, failed) = acc
            UrlParts(it) match {
              case Success(urlParts) =>
                (succeeded + urlParts, failed)
              case Failure(e: MalformedURLException) =>
                (succeeded, failed + it)
              case Failure(other) =>
                sender ! InternalProblemResponse
                throw other
            }
        }
        if (failed.isEmpty) {
          val geminiReq = GeminiRequest.Ask(id, GeminiRequestType.WEAK, succeeded.map(_.url))
          geminiActor ! geminiReq
          waitMap += (geminiReq -> RequestInfo(req, sender(), succeeded))
          waitQueue = waitQueue enqueue (config.timeout.fromNow -> geminiReq)
          context.system.scheduler.scheduleOnce(config.timeout, self, PING)
        } else {
          sender ! UserProblemResponse(req, UserProblem.MalformedUrls(failed))
        }

      case resp@GeminiResponse.Answer(req, urls) if waitMap.contains(req) =>
        val reqInfo = waitMap(req)
        waitMap -= req
        Try {
          reqInfo.urls.map {
            srcUrl =>
              val newUrl = urls(srcUrl.url)
              val newUrlParts = UrlParts(newUrl).get
              NormalizedUrl(srcUrl.host, newUrlParts.host, newUrlParts.rel, srcUrl.rel)
          }
        } match {
          case Success(normalizedUrls) =>
            reqInfo.client ! NormalizeResponse(reqInfo.req, normalizedUrls)
          case Failure(e) =>
            logWithId.error(reqInfo.req.id, e, "Url normalization failed")
            reqInfo.client ! FailureResponse(reqInfo.req, e)
        }
      case PING =>
        val (expired, rest) = waitQueue.span(_._1.isOverdue())
        expired.foreach {
          case (deadline, req) =>
            waitMap.get(req).foreach {
              reqInfo =>
                logWithId.error(reqInfo.req.id, "Url normalization timeout expired for request " + reqInfo)
                reqInfo.client ! FailureResponse(reqInfo.req, InternalProblem.TemporarilyProblem)
            }
        }
        waitMap --= expired.map(_._2)
        waitQueue = rest
    }
  }

  object UrlNormalizer {
    def props(config: UrlNormalizerConfig) = Props(classOf[UrlNormalizer], component, config)
  }

  object UrlNormalizerConfig {

    import ru.yandex.atom.utils.config._

    implicit def apply(config: Config): UrlNormalizerConfig = UrlNormalizerConfig(
      timeout = config.getFiniteDuration("timeout")
    )
  }

  case class UrlNormalizerConfig(timeout: FiniteDuration)

}
