/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package org.jboss.as.webservices.webserviceref;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.xml.namespace.QName;
import jakarta.xml.ws.Service;
import jakarta.xml.ws.WebServiceRef;
import jakarta.xml.ws.WebServiceRefs;
import jakarta.xml.ws.soap.MTOM;

import org.jboss.as.webservices.logging.WSLogger;
import org.jboss.metadata.javaee.jboss.JBossPortComponentRef;
import org.jboss.metadata.javaee.jboss.JBossServiceReferenceMetaData;
import org.jboss.metadata.javaee.jboss.StubPropertyMetaData;
import org.jboss.metadata.javaee.spec.Addressing;
import org.jboss.metadata.javaee.spec.ParamValueMetaData;
import org.jboss.metadata.javaee.spec.PortComponentRef;
import org.jboss.metadata.javaee.spec.ServiceReferenceHandlerChainMetaData;
import org.jboss.metadata.javaee.spec.ServiceReferenceHandlerChainsMetaData;
import org.jboss.metadata.javaee.spec.ServiceReferenceHandlerMetaData;
import org.jboss.metadata.javaee.spec.ServiceReferenceMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.AddressingMetadata;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.MTOMMetadata;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.RespectBindingMetadata;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerChainMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerChainsMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedInitParamMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedPortComponentRefMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedPortComponentRefMetaDataBuilder;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedServiceRefMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedServiceRefMetaDataBuilder;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedStubPropertyMetaData;

/**
 * Translates WS Refs from JBossAS MD to JBossWS UMDM format.
 *
 * Some of the methods on this class are public to allow weld
 *
 * @author <a href="mailto:ropalka@redhat.com">Richard Opalka</a>
 */
final class WSRefUtils {

    private WSRefUtils() {
    }

    static UnifiedServiceRefMetaData translate(final ServiceReferenceMetaData serviceRefMD) {
        UnifiedServiceRefMetaDataBuilder builder = new UnifiedServiceRefMetaDataBuilder();
        builder.setServiceRefName(serviceRefMD.getName());
        builder.setServiceRefType(serviceRefMD.getServiceRefType());
        builder.setServiceInterface(serviceRefMD.getServiceInterface());
        builder.setWsdlFile(serviceRefMD.getWsdlFile());
        builder.setMappingFile(serviceRefMD.getJaxrpcMappingFile());
        builder.setServiceQName(serviceRefMD.getServiceQname());

        // propagate port components
        final Collection<? extends PortComponentRef> portComponentsMD = serviceRefMD.getPortComponentRef();
        if (portComponentsMD != null) {
            for (final PortComponentRef portComponentMD : portComponentsMD) {
                final UnifiedPortComponentRefMetaData portComponentUMDM = getUnifiedPortComponentRefMetaData(portComponentMD);
                if (portComponentUMDM.getServiceEndpointInterface() != null || portComponentUMDM.getPortQName() != null) {
                    builder.addPortComponentRef(portComponentUMDM);
                } else {
                    WSLogger.ROOT_LOGGER.ignoringPortComponentRef(portComponentUMDM);
                }
            }
        }

        // propagate handlers
        final Collection<ServiceReferenceHandlerMetaData> handlersMD = serviceRefMD.getHandlers();
        if (handlersMD != null) {
           for (final ServiceReferenceHandlerMetaData handlerMD : handlersMD) {
              final UnifiedHandlerMetaData handlerUMDM = getUnifiedHandlerMetaData(handlerMD);
              builder.addHandler(handlerUMDM);
           }
        }

        // propagate handler chains
        ServiceReferenceHandlerChainsMetaData handlerChainsMD = serviceRefMD.getHandlerChains();
        if (handlerChainsMD != null) {
           final UnifiedHandlerChainsMetaData handlerChainsUMDM = getUnifiedHandlerChainsMetaData(handlerChainsMD);
           builder.setHandlerChains(handlerChainsUMDM);
        }

        // propagate jboss specific MD
        if (serviceRefMD instanceof JBossServiceReferenceMetaData) {
           final JBossServiceReferenceMetaData jbossServiceRefMD = (JBossServiceReferenceMetaData) serviceRefMD;
           builder.setServiceImplClass(jbossServiceRefMD.getServiceClass());
           builder.setConfigName(jbossServiceRefMD.getConfigName());
           builder.setConfigFile(jbossServiceRefMD.getConfigFile());
           builder.setWsdlOverride(jbossServiceRefMD.getWsdlOverride());
           builder.setHandlerChain(jbossServiceRefMD.getHandlerChain());
        }

        return builder.build();
    }

    private static UnifiedPortComponentRefMetaData getUnifiedPortComponentRefMetaData(final PortComponentRef portComponentMD) {
        final UnifiedPortComponentRefMetaDataBuilder builder = new UnifiedPortComponentRefMetaDataBuilder();

        // propagate service endpoint interface
        builder.setServiceEndpointInterface(portComponentMD.getServiceEndpointInterface());

        // propagate MTOM properties
        builder.setMtomEnabled(portComponentMD.isEnableMtom());
        builder.setMtomThreshold(portComponentMD.getMtomThreshold());

        // propagate addressing properties
        final Addressing addressingMD = portComponentMD.getAddressing();
        if (addressingMD != null) {
            builder.setAddressingAnnotationSpecified(true);
            builder.setAddressingEnabled(addressingMD.isEnabled());
            builder.setAddressingRequired(addressingMD.isRequired());
            builder.setAddressingResponses(addressingMD.getResponses());
        }

        // propagate respect binding properties
        if (portComponentMD.getRespectBinding() != null) {
            builder.setRespectBindingAnnotationSpecified(true);
            builder.setRespectBindingEnabled(true);
        }

        // propagate link
        builder.setPortComponentLink(portComponentMD.getPortComponentLink());

        // propagate jboss specific MD
        if (portComponentMD instanceof JBossPortComponentRef) {
            final JBossPortComponentRef jbossPortComponentMD = (JBossPortComponentRef) portComponentMD;

            // propagate port QName
            builder.setPortQName(jbossPortComponentMD.getPortQname());

            // propagate configuration properties
            builder.setConfigName(jbossPortComponentMD.getConfigName());
            builder.setConfigFile(jbossPortComponentMD.getConfigFile());

            // propagate stub properties
            final List<StubPropertyMetaData> stubPropertiesMD = jbossPortComponentMD.getStubProperties();
            if (stubPropertiesMD != null) {
                for (final StubPropertyMetaData stubPropertyMD : stubPropertiesMD) {
                    builder.addStubProperty(new UnifiedStubPropertyMetaData(stubPropertyMD.getPropName(), stubPropertyMD.getPropValue()));
                }
            }
        }

        return builder.build();
    }

    private static UnifiedHandlerMetaData getUnifiedHandlerMetaData(ServiceReferenceHandlerMetaData srhmd) {
        List<UnifiedInitParamMetaData> unifiedInitParamMDs = new LinkedList<UnifiedInitParamMetaData>();
        List<ParamValueMetaData> initParams = srhmd.getInitParam();
        if (initParams != null) {
            for (ParamValueMetaData initParam : initParams) {
                unifiedInitParamMDs.add(new UnifiedInitParamMetaData(initParam.getParamName(), initParam.getParamValue()));
            }
        }
        List<QName> soapHeaders = srhmd.getSoapHeader();
        Set<QName> soapHeaderList = soapHeaders != null ? new HashSet<QName>(soapHeaders) : null;
        List<String> soapRoles = srhmd.getSoapRole();
        Set<String> soapRolesList = soapRoles != null ? new HashSet<String>(soapRoles) : null;
        List<String> portNames = srhmd.getPortName();
        Set<String> portNameList = portNames != null ? new HashSet<String>(portNames) : null;
        return new UnifiedHandlerMetaData(srhmd.getHandlerClass(), srhmd.getHandlerName(), unifiedInitParamMDs, soapHeaderList, soapRolesList, portNameList);
    }

    private static UnifiedHandlerChainsMetaData getUnifiedHandlerChainsMetaData(final ServiceReferenceHandlerChainsMetaData handlerChainsMD) {
        List<UnifiedHandlerChainMetaData> uhcmds = new LinkedList<UnifiedHandlerChainMetaData>();
        for (final ServiceReferenceHandlerChainMetaData handlerChainMD : handlerChainsMD.getHandlers()) {
            List<UnifiedHandlerMetaData> uhmds = new LinkedList<UnifiedHandlerMetaData>();
            for (final ServiceReferenceHandlerMetaData handlerMD : handlerChainMD.getHandler()) {
                final UnifiedHandlerMetaData handlerUMDM = getUnifiedHandlerMetaData(handlerMD);
                uhmds.add(handlerUMDM);
            }
            uhcmds.add(new UnifiedHandlerChainMetaData(handlerChainMD.getServiceNamePattern(), handlerChainMD.getPortNamePattern(),
                    handlerChainMD.getProtocolBindings(), uhmds, false, null));
        }

        return new UnifiedHandlerChainsMetaData(uhcmds);
    }

    static void processAnnotatedElement(final AnnotatedElement anElement, final UnifiedServiceRefMetaData serviceRefUMDM) {
       processAddressingAnnotation(anElement, serviceRefUMDM);
       processMTOMAnnotation(anElement, serviceRefUMDM);
       processRespectBindingAnnotation(anElement, serviceRefUMDM);
       processHandlerChainAnnotation(anElement, serviceRefUMDM);
       processServiceRefType(anElement, serviceRefUMDM);
    }

    private static void processAddressingAnnotation(final AnnotatedElement anElement, final UnifiedServiceRefMetaData serviceRefUMDM) {
         final jakarta.xml.ws.soap.Addressing addressingAnnotation = getAnnotation(anElement, jakarta.xml.ws.soap.Addressing.class);

         if (addressingAnnotation != null) {
             serviceRefUMDM.setAddressingMedadata(new AddressingMetadata(true,
                    addressingAnnotation.enabled(),
                    addressingAnnotation.required(),
                    addressingAnnotation.responses().toString()));
         }
      }

      private static void processMTOMAnnotation(final AnnotatedElement anElement, final UnifiedServiceRefMetaData serviceRefUMDM) {
         final MTOM mtomAnnotation = getAnnotation(anElement, MTOM.class);

         if (mtomAnnotation != null) {
            serviceRefUMDM.setMTOMMetadata(new MTOMMetadata(true, mtomAnnotation.enabled(), mtomAnnotation.threshold()));
         }
      }

      private static void processRespectBindingAnnotation(final AnnotatedElement anElement, final UnifiedServiceRefMetaData serviceRefUMDM) {
         final jakarta.xml.ws.RespectBinding respectBindingAnnotation = getAnnotation(anElement, jakarta.xml.ws.RespectBinding.class);

         if (respectBindingAnnotation != null) {
            serviceRefUMDM.setRespectBindingMetadata(new RespectBindingMetadata(true, respectBindingAnnotation.enabled()));
         }
      }

      private static  void processServiceRefType(final AnnotatedElement anElement, final UnifiedServiceRefMetaData serviceRefUMDM) {
         if (anElement instanceof Field) {
            final Class<?> targetClass = ((Field) anElement).getType();
            serviceRefUMDM.setServiceRefType(targetClass.getName());

            if (Service.class.isAssignableFrom(targetClass))
               serviceRefUMDM.setServiceInterface(targetClass.getName());
         } else if (anElement instanceof Method) {
            final Class<?> targetClass = ((Method) anElement).getParameterTypes()[0];
            serviceRefUMDM.setServiceRefType(targetClass.getName());

            if (Service.class.isAssignableFrom(targetClass))
               serviceRefUMDM.setServiceInterface(targetClass.getName());
         } else {
            final WebServiceRef serviceRefAnnotation = getWebServiceRefAnnotation(anElement, serviceRefUMDM);
            Class<?> targetClass = null;
            if (serviceRefAnnotation != null && (serviceRefAnnotation.type() != Object.class))
            {
               targetClass = serviceRefAnnotation.type();
               serviceRefUMDM.setServiceRefType(targetClass.getName());

               if (Service.class.isAssignableFrom(targetClass))
                  serviceRefUMDM.setServiceInterface(targetClass.getName());
            }
         }
      }

      private static void processHandlerChainAnnotation(final AnnotatedElement anElement, final UnifiedServiceRefMetaData serviceRefUMDM) {
         final jakarta.jws.HandlerChain handlerChainAnnotation = getAnnotation(anElement, jakarta.jws.HandlerChain.class);

         if (handlerChainAnnotation != null) {
            // Set the handlerChain from @HandlerChain on the annotated element
            String handlerChain = null;
            if (handlerChainAnnotation.file().length() > 0)
               handlerChain = handlerChainAnnotation.file();

            // Resolve path to handler chain
            if (handlerChain != null) {
               try {
                  new URL(handlerChain);
               } catch (MalformedURLException ignored) {
                  final Class<?> declaringClass = getDeclaringClass(anElement);
                  if (declaringClass != null) {
                      handlerChain = declaringClass.getPackage().getName().replace('.', '/') + "/" + handlerChain;
                  }
               }

               serviceRefUMDM.setHandlerChain(handlerChain);
            }
         }
      }

      private static <T extends Annotation> T getAnnotation(final AnnotatedElement anElement, final Class<T> annotationClass) {
         return anElement != null ? (T) anElement.getAnnotation(annotationClass) : null;
      }

      private static Class<?> getDeclaringClass(final AnnotatedElement annotatedElement) {
         Class<?> declaringClass = null;
         if (annotatedElement instanceof Field) {
            declaringClass = ((Field) annotatedElement).getDeclaringClass();
         } else if (annotatedElement instanceof Method) {
            declaringClass = ((Method) annotatedElement).getDeclaringClass();
         } else if (annotatedElement instanceof Class) {
            declaringClass = (Class<?>) annotatedElement;
         }

         return declaringClass;
      }

      private static WebServiceRef getWebServiceRefAnnotation(final AnnotatedElement anElement, final UnifiedServiceRefMetaData serviceRefUMDM) {
          final WebServiceRef webServiceRefAnnotation = getAnnotation(anElement, WebServiceRef.class);
          final WebServiceRefs webServiceRefsAnnotation = getAnnotation(anElement, WebServiceRefs.class);

          if (webServiceRefAnnotation == null && webServiceRefsAnnotation == null) {
              return null;
          }

          // Build the list of @WebServiceRef relevant annotations
          final List<WebServiceRef> wsrefList = new ArrayList<WebServiceRef>();

          if (webServiceRefAnnotation != null) {
              wsrefList.add(webServiceRefAnnotation);
          }

          if (webServiceRefsAnnotation != null) {
              for (final WebServiceRef webServiceRefAnn : webServiceRefsAnnotation.value()) {
                  wsrefList.add(webServiceRefAnn);
              }
          }

          // Return effective @WebServiceRef annotation
          WebServiceRef returnValue = null;
          if (wsrefList.size() == 1) {
              returnValue = wsrefList.get(0);
          } else {
              for (WebServiceRef webServiceRefAnn : wsrefList) {
                  if (serviceRefUMDM.getServiceRefName().endsWith(webServiceRefAnn.name())) {
                      returnValue = webServiceRefAnn;
                      break;
                  }
              }
          }

          return returnValue;
      }

}
