content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you 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 org.apache.causeway.viewer.graphql.viewer.controller;
  
  import java.util.Objects;
  import java.util.Optional;
  
  import jakarta.inject.Inject;
  
  import org.jspecify.annotations.Nullable;
  
  import org.springframework.http.ContentDisposition;
  import org.springframework.http.HttpHeaders;
  import org.springframework.http.HttpStatus;
  import org.springframework.http.MediaType;
  import org.springframework.http.ResponseEntity;
  import org.springframework.util.MimeType;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.PathVariable;
  import org.springframework.web.bind.annotation.RequestMapping;
  import org.springframework.web.bind.annotation.RestController;
  
  import org.apache.causeway.applib.layout.grid.Grid;
  import org.apache.causeway.applib.services.bookmark.Bookmark;
  import org.apache.causeway.applib.services.bookmark.BookmarkService;
  import org.apache.causeway.applib.value.Blob;
  import org.apache.causeway.applib.value.Clob;
  import org.apache.causeway.commons.io.JaxbUtils;
  import org.apache.causeway.core.config.CausewayConfiguration;
  import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet;
  import org.apache.causeway.core.metamodel.object.ManagedObject;
  import org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
  import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
  
  @RestController()
  @RequestMapping("/graphql/object")
  public class ResourceController {
  
      private final BookmarkService bookmarkService;
      private final ObjectManager objectManager;
      private final CausewayConfiguration.Viewer.Graphql graphqlConfiguration;
  
      @Inject
      public ResourceController(
              final BookmarkService bookmarkService,
              final ObjectManager objectManager,
              final CausewayConfiguration causewayConfiguration) {
          this.bookmarkService = bookmarkService;
          this.objectManager = objectManager;
          this.graphqlConfiguration = causewayConfiguration.getViewer().getGraphql();
      }
  
      @GetMapping(value = "/{logicalTypeName}:{id}/{propertyId}/blobBytes")
      public ResponseEntity<byte[]> propertyBlobBytes(
              @PathVariable final String logicalTypeName,
              @PathVariable final String id,
              @PathVariable final String propertyId
      ) {
          var responseType = graphqlConfiguration.getResources().getResponseType();
  
          // TODO: perhaps a filter would factor this check out?
          if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN) {
              return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
          }
  
          return valueOfProperty(logicalTypeName, id, propertyId)
                  .filter(Blob.class::isInstance)
                  .map(Blob.class::cast)
                  .map(blob -> {
                      var bodyBuilder = ResponseEntity.ok()
                              .contentType(MediaType.asMediaType(MimeType.valueOf(blob.mimeType().toString())));
                      if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.ATTACHMENT) {
                          bodyBuilder
                                  .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(blob.name()).build().toString())
                                  .contentLength(blob.bytes().length);
                      }
                      return bodyBuilder.body(blob.bytes());
                  })
                  .orElse(ResponseEntity.notFound().build());
      }
  
      @GetMapping(value = "/{logicalTypeName}:{id}/{propertyId}/clobChars")
      public ResponseEntity<CharSequence> propertyClobChars(
              @PathVariable final String logicalTypeName,
              @PathVariable final String id,
              @PathVariable final String propertyId
      ) {
          var responseType = graphqlConfiguration.getResources().getResponseType();
  
          // TODO: perhaps a filter would factor this check out?
          if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN) {
              return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
          }
  
          return valueOfProperty(logicalTypeName, id, propertyId)
                  .filter(Clob.class::isInstance)
                  .map(Clob.class::cast)
                  .map(clob -> {
                      var bodyBuilder = ResponseEntity.ok()
                              .contentType(MediaType.asMediaType(MimeType.valueOf(clob.mimeType().toString())));
                      if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.ATTACHMENT) {
                          bodyBuilder.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(clob.name()).build().toString())
                                  .contentLength(clob.chars().length());
                      }
                      return bodyBuilder.body(clob.chars());
                  })
                  .orElse(ResponseEntity.notFound().build());
      }
  
      @GetMapping(value = "/{logicalTypeName}:{id}/{_meta}/grid")
      public ResponseEntity<String> grid(
              @PathVariable final String logicalTypeName,
              @PathVariable final String id,
              @PathVariable final String _meta
      ) {
          var responseType = graphqlConfiguration.getResources().getResponseType();
  
          // TODO: perhaps a filter would factor this check out?
          if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN) {
              return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
          }
  
          if (!_meta.equals(graphqlConfiguration.getMetaData().getFieldName())) {
              return ResponseEntity.notFound().build();
          }
  
          return lookup(logicalTypeName, id)
                  .map(ResourceController::gridOf)
                  .filter(Objects::nonNull)
                  .map(JaxbUtils::toStringUtf8)
                  .map(x -> x.replaceAll("(\r\n)", "\n"))
                  .map(gridText -> {
                      var bodyBuilder = ResponseEntity.ok()
                              .contentType(MediaType.APPLICATION_XML);
                      if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.ATTACHMENT) {
                          bodyBuilder
                                  .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(logicalTypeName + ".layout.xml").build().toString())
                                  .contentLength(gridText.length());
                      }
                      return bodyBuilder.body(gridText);
                  })
                  .orElse(ResponseEntity.notFound().build());
      }
  
      @GetMapping(value = "/{logicalTypeName}:{id}/{_meta}/icon")
      public ResponseEntity<byte[]> icon(
              @PathVariable final String logicalTypeName,
              @PathVariable final String id,
              @PathVariable final String _meta
      ) {
          // TODO: perhaps a filter would factor this check out?
          var responseType = graphqlConfiguration.getResources().getResponseType();
          if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.FORBIDDEN) {
              return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
          }
          if (!_meta.equals(graphqlConfiguration.getMetaData().getFieldName())) {
              return ResponseEntity.notFound().build();
          }
  
          return lookup(logicalTypeName, id)
                  .map(ManagedObject::getIcon)
                  .filter(Objects::nonNull)
                  .map(objectIcon -> {
                      var bytes = objectIcon.asBytes();
                      var bodyBuilder = ResponseEntity.ok()
                              .contentType(MediaType.parseMediaType(objectIcon.getMimeType().getMimeType().toString()));
                      if (responseType == CausewayConfiguration.Viewer.Graphql.ResponseType.ATTACHMENT) {
                          bodyBuilder
                                  .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(logicalTypeName + ".png").build().toString())
                                  .contentLength(bytes.length);
                      }
                      return bodyBuilder.body(bytes);
                  })
                  .orElse(ResponseEntity.notFound().build());
      }
  
      @Nullable
      private static Grid gridOf(final ManagedObject managedObject) {
          var facet = managedObject.objSpec().getFacet(GridFacet.class);
          return facet != null ? facet.getGrid(managedObject) : null;
      }
  
      private Optional<Object> valueOfProperty(final String logicalTypeName, final String id, final String propertyId) {
          return lookup(logicalTypeName, id)
                  .map(managedObject -> new ManagedObjectAndPropertyIfAny(managedObject, managedObject.objSpec().getProperty(propertyId)))
                  .filter(ManagedObjectAndPropertyIfAny::isPropertyPresent)
                  .map(ManagedObjectAndProperty::of)
                  .map(ManagedObjectAndProperty::value)
                  .map(ManagedObject::getPojo);
      }
  
      private Optional<ManagedObject> lookup(final String logicalTypeName, final String id) {
          return bookmarkService.lookup(Bookmark.forLogicalTypeNameAndIdentifier(logicalTypeName, id))
                  .map(objectManager::adapt);
      }
  
      private record ManagedObjectAndPropertyIfAny(
              ManagedObject owningObject,
              Optional<OneToOneAssociation> propertyIfAny) {
          boolean isPropertyPresent() {
              return propertyIfAny.isPresent();
          }
      }
  
      private static class ManagedObjectAndProperty {
          private static ManagedObjectAndProperty of(final ManagedObjectAndPropertyIfAny tuple) {
              return new ManagedObjectAndProperty(tuple);
          }
          private ManagedObjectAndProperty(final ManagedObjectAndPropertyIfAny tuple) {
              this.owningObject = tuple.owningObject;
              this.property = tuple.propertyIfAny.orElse(null);
          }
          ManagedObject owningObject;
          OneToOneAssociation property;
  
          ManagedObject value() {
              return property.get(owningObject);
          }
      }
  }
  