package org.jboss.resteasy.client.jaxrs.cache;

import org.jboss.resteasy.resteasy_jaxrs.i18n.LogMessages;
import org.jboss.resteasy.util.DateUtil;
import org.jboss.resteasy.util.MediaTypeHelper;
import org.jboss.resteasy.util.ReadFromStream;

import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.client.ClientResponseContext;
import jakarta.ws.rs.client.ClientResponseFilter;
import jakarta.ws.rs.core.CacheControl;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
@SuppressWarnings("unchecked")
public class CacheInterceptor implements ClientRequestFilter, ClientResponseFilter
{
   protected BrowserCache cache;

   public CacheInterceptor(final BrowserCache cache)
   {
      LogMessages.LOGGER.debugf("Interceptor : %s,  Method : CacheInterceptor", getClass().getName());
      this.cache = cache;
   }

   @Override
   public void filter(ClientRequestContext request) throws IOException
   {
      if (!request.getMethod().equalsIgnoreCase("GET")) return;
      try
      {
         BrowserCache.Entry entry = getEntry(request);
         if (entry == null) return;
         if (entry.expired())
         {
            // entry should have a remove method
            cache.remove(request.getUri().toString(), entry.getMediaType());

            // add validation headers
            BrowserCache.Header[] headers = entry.getValidationHeaders();
            for (BrowserCache.Header header : headers)
            {
               request.getHeaders().putSingle(header.getName(), header.getValue());
            }
            request.setProperty("expired.cache.entry", entry);
            return;
         }
         request.setProperty("cached", "cached");
         request.abortWith(cachedResponse(entry));
      }
      catch (IOException io)
      {
         throw io;
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }

   private Response cachedResponse(BrowserCache.Entry entry)
   {
      ByteArrayInputStream bais = new ByteArrayInputStream(entry.getCached());
      Response.ResponseBuilder builder = Response.ok().entity(bais);
      for (Map.Entry<String, List<String>> header : entry.getHeaders().entrySet())
      {
         for (String val : header.getValue())
         {
            builder.header(header.getKey(), val);
         }
      }
      return builder.build();
   }

   @Override
   public void filter(ClientRequestContext request, ClientResponseContext response) throws IOException
   {
      if (!request.getMethod().equalsIgnoreCase("GET") || request.hasProperty("cached")) return;
      else if (response.getStatus() == 304)
      {
         BrowserCache.Entry entry = (BrowserCache.Entry)request.getProperty("expired.cache.entry");
         updateOnNotModified(request, entry, response);
         return;
      }
      else if (response.getStatus() == 200)
      {
         cache(request, response);
      }
   }

   private void useCacheEntry(ClientResponseContext response, BrowserCache.Entry entry)
   {
      ByteArrayInputStream bais = new ByteArrayInputStream(entry.getCached());
      response.setEntityStream(bais);
      response.setStatus(200);

      for (Map.Entry<String, List<String>> header : entry.getHeaders().entrySet())
      {
         response.getHeaders().remove(header.getKey());
         for (String val : header.getValue())
         {
            response.getHeaders().add(header.getKey(), val);
         }
      }
   }

   private void cache(ClientRequestContext request, ClientResponseContext response) throws IOException
   {
      if (response.getStatus() != 200) return;
      cacheIfPossible(request, response);
   }

   public void updateOnNotModified(ClientRequestContext request, BrowserCache.Entry old, ClientResponseContext response)
   {
      old.getHeaders().remove(HttpHeaders.CACHE_CONTROL);
      old.getHeaders().remove(HttpHeaders.EXPIRES);
      old.getHeaders().remove(HttpHeaders.LAST_MODIFIED);
      String cc = (String) response.getHeaderString(HttpHeaders.CACHE_CONTROL);
      String exp = (String) response.getHeaderString(HttpHeaders.EXPIRES);
      int expires = -1;

      if (cc != null)
      {
         CacheControl cacheControl = CacheControl.valueOf(cc);
         if (cacheControl.isNoCache())
         {
            useCacheEntry(response, old);
            return;
         }
         expires = cacheControl.getMaxAge();
      }
      else if (exp != null)
      {
         Date date = DateUtil.parseDate(exp);
         expires = (int) ((date.getTime() - System.currentTimeMillis()) / 1000);
      }

      if (cc != null)
      {
         old.getHeaders().putSingle(HttpHeaders.CACHE_CONTROL, cc);
      }
      if (exp != null)
      {
         old.getHeaders().putSingle(HttpHeaders.CACHE_CONTROL, exp);
      }

      String lastModified = (String) response.getHeaderString(HttpHeaders.LAST_MODIFIED);
      String etag = (String) response.getHeaderString(HttpHeaders.ETAG);

      if (etag == null) etag = old.getHeaders().getFirst(HttpHeaders.ETAG);
      else old.getHeaders().putSingle(HttpHeaders.ETAG, etag);

      if (lastModified != null)
      {
         old.getHeaders().putSingle(HttpHeaders.LAST_MODIFIED, lastModified);
      }

      if (etag == null && lastModified == null && cc == null && exp == null) // don't cache
      {
         useCacheEntry(response, old);
         return;
      }


      BrowserCache.Entry entry = cache.put(request.getUri().toString(), old.getMediaType(), old.getHeaders(), old.getCached(), expires, etag, lastModified);
      useCacheEntry(response, entry);

   }



   public void cacheIfPossible(ClientRequestContext request, ClientResponseContext response) throws IOException
   {
      String cc = (String) response.getHeaderString(HttpHeaders.CACHE_CONTROL);
      String exp = (String) response.getHeaderString(HttpHeaders.EXPIRES);
      int expires = -1;

      if (cc != null)
      {
         CacheControl cacheControl = CacheControl.valueOf(cc);
         if (cacheControl.isNoCache()) return;
         expires = cacheControl.getMaxAge();
      }
      else if (exp != null)
      {
         Date date = DateUtil.parseDate(exp);
         expires = (int) ((date.getTime() - System.currentTimeMillis()) / 1000);
      }

      String lastModified = (String) response.getHeaderString(HttpHeaders.LAST_MODIFIED);
      String etag = (String) response.getHeaderString(HttpHeaders.ETAG);

      String contentType = (String) response.getHeaderString(HttpHeaders.CONTENT_TYPE);
      String accept = (String) request.getHeaderString(HttpHeaders.ACCEPT);

      byte[] cached = ReadFromStream.readFromStream(1024, response.getEntityStream());
      // if Accept is present, use it, if not fallback to response Content-Type
      MediaType mediaType = MediaType.valueOf(contentType);
      if (accept != null) {
         for (String acceptItem : accept.split("\\s*,\\s*")) {
            MediaType acceptMediaType = MediaType.valueOf(acceptItem);
            if (mediaType.isCompatible(acceptMediaType)) {
               mediaType = acceptMediaType;
               break;
            }
         }
      }

      final BrowserCache.Entry entry = cache.put(request.getUri().toString(), mediaType,
              response.getHeaders(), cached, expires, etag, lastModified);

      response.setEntityStream(new ByteArrayInputStream(cached));
   }

   protected BrowserCache.Entry getEntry(ClientRequestContext request) throws Exception
   {
      String uri = request.getUri().toString();

      List<MediaType> acceptableMediaTypes = request.getAcceptableMediaTypes();
      BrowserCache.Entry entry = null;
      if (acceptableMediaTypes.size() > 0)
      {
         for (MediaType accept : acceptableMediaTypes)
         {
            entry = cache.get(uri, accept);
            if (entry != null) return entry;
            if (MediaTypeHelper.isTextLike(accept))
            {
               entry = cache.get(uri, accept.withCharset("UTF-8"));
               if (entry != null) return entry;
            }
         }

      }
      else
      {
         return cache.getAny(uri);
      }

      return null;
   }

}
