// Copyright © 2012-2020 VLINGO LABS. All rights reserved.
//
// This Source Code Form is subject to the terms of the
// Mozilla Public License, v. 2.0. If a copy of the MPL
// was not distributed with this file, You can obtain
// one at https://mozilla.org/MPL/2.0/.

package io.vlingo.http.resource.sse;

import static io.vlingo.http.Response.Status.Ok;
import static io.vlingo.http.ResponseHeader.correlationId;
import static io.vlingo.http.ResponseHeader.headers;

import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import io.vlingo.actors.Actor;
import io.vlingo.actors.ActorInstantiator;
import io.vlingo.actors.ActorInstantiatorRegistry;
import io.vlingo.actors.Definition;
import io.vlingo.actors.Stoppable;
import io.vlingo.actors.World;
import io.vlingo.common.Cancellable;
import io.vlingo.common.Scheduled;
import io.vlingo.http.Header.Headers;
import io.vlingo.http.Method;
import io.vlingo.http.Request;
import io.vlingo.http.RequestHeader;
import io.vlingo.http.Response;
import io.vlingo.http.ResponseHeader;
import io.vlingo.http.resource.ResourceHandler;
import io.vlingo.http.resource.sse.SsePublisher.SsePublisherInstantiator;
import io.vlingo.wire.channel.RequestResponseContext;

public class SseStreamResource extends ResourceHandler {
  private static final Map<String,SsePublisher> publishers = new ConcurrentHashMap<>();
  private final World world;

  public SseStreamResource(final World world) {
    this.world = world;
  }

  public void subscribeToStream(final String streamName, final Class<? extends Actor> feedClass, final int feedPayload, final int feedInterval, final String feedDefaultId) {
    final RequestResponseContext<?> clientContext = context().clientContext();

    clientContext.whenClosing(unsubscribeRequest());

    final String correlationId = context().request().headerValueOr(RequestHeader.XCorrelationID, "");
    final Headers<ResponseHeader> headers = headers(correlationId(correlationId));

    final SseSubscriber subscriber =
            new SseSubscriber(
                    streamName,
                    new SseClient(clientContext, headers),
                    correlationId,
                    context().request().headerValueOr(RequestHeader.LastEventID, ""));

    publisherFor(streamName, feedClass, feedPayload, feedInterval, feedDefaultId).subscribe(subscriber);
  }

  public void unsubscribeFromStream(final String streamName, final String id) {
    final SsePublisher publisher = publishers.get(streamName);
    if (publisher != null) {
      publisher.unsubscribe(new SseSubscriber(streamName, new SseClient(context().clientContext())));
    }

    completes().with(Response.of(Ok));
  }

  private SsePublisher publisherFor(final String streamName, final Class<? extends Actor> feedClass, final int feedPayload, final int feedInterval, final String feedDefaultId) {
    SsePublisher publisher = publishers.get(streamName);
    if (publisher == null) {
      publisher = world.actorFor(SsePublisher.class, Definition.has(SsePublisherActor.class, new SsePublisherInstantiator(streamName, feedClass, feedPayload, feedInterval, feedDefaultId)));
      final SsePublisher presentPublisher = publishers.putIfAbsent(streamName, publisher);
      if (presentPublisher != null) {
        publisher.stop();
        publisher = presentPublisher;
      }
    }
    return publisher;
  }

  private Request unsubscribeRequest() {
    try {
      final String unsubscribePath = context().request().uri.getPath() + "/" + context().clientContext().id();
      return Request.has(Method.DELETE).and(new URI(unsubscribePath));
    } catch (Exception e) {
      return null;
    }
  }


  //=====================================
  // SsePublisherActor
  //=====================================

  public static class SsePublisherActor extends Actor implements SsePublisher, Scheduled<Object>, Stoppable {
    private final Cancellable cancellable;
    private final SseFeed feed;
    private final String streamName;
    private final Map<String,SseSubscriber> subscribers;

    @SuppressWarnings("unchecked")
    public SsePublisherActor(final String streamName, final Class<? extends Actor> feedClass, final int feedPayload, final int feedInterval, final String feedDefaultId) {
      this.streamName = streamName;
      this.subscribers = new HashMap<>();

      final ActorInstantiator<?> instantiator = ActorInstantiatorRegistry.instantiatorFor(feedClass);
      if(instantiator==null)throw new IllegalArgumentException("No ActorInstantiator registred for feedClass="+feedClass.toString());
      instantiator.set("feedClass", feedClass);
      instantiator.set("streamName", streamName);
      instantiator.set("feedPayload", feedPayload);
      instantiator.set("feedDefaultId", feedDefaultId);

      this.feed = stage().actorFor(SseFeed.class, Definition.has(feedClass, instantiator));

      this.cancellable = stage().scheduler().schedule(selfAs(Scheduled.class), null, 10, feedInterval);

      logger().info("SsePublisher started for: " + this.streamName);
    }


    //=====================================
    // SsePublisher
    //=====================================

    @Override
    public void subscribe(final SseSubscriber subscriber) {
      subscribers.put(subscriber.id(), subscriber);
    }

    @Override
    public void unsubscribe(final SseSubscriber subscriber) {
      final SseSubscriber actual = subscribers.remove(subscriber.id());
      if (actual != null) {
        actual.close();
      }
    }


    //=====================================
    // Scheduled
    //=====================================

    @Override
    public void intervalSignal(final Scheduled<Object> scheduled, final Object data) {
      feed.to(subscribers.values());
    }


    //=====================================
    // Stoppable
    //=====================================

    @Override
    public void stop() {
      cancellable.cancel();

      unsubscribeAll();

      super.stop();
    }

    private void unsubscribeAll() {
      final Collection<SseSubscriber> all = subscribers.values();
      for (final SseSubscriber subscriber : all.toArray(new SseSubscriber[all.size()])) {
        unsubscribe(subscriber);
      }
    }
  }
}