text: ##
  ## © Copyright 2021- IBM Inc. All rights reserved
  # SPDX-License-Identifier: MIT
  ##
  
  
  import logging
  import urllib
  
  import requests.exceptions
  
  from . import rdfxml
  from . import oslcqueryapi
  from . import utils
  from . import httpops
  
  logger = logging.getLogger(__name__)
  
  #################################################################################################
  # a generic jazz application
  
  class _App( httpops.HttpOperations_Mixin ):
      'A generic Jazz application'
      domain = 'UNSPECIFIED APP DOMAIN'
      project_class = None
      artifact_formats = [] # For RR
      reportablerest_baseurl = "publish"
      supports_reportable_rest = False
      reportable_rest_status = "Not supported by application"
      majorVersion = None
      version = None
  
      def __init__(self, server, contextroot, *, jts=None):
          super().__init__()
          logger.info( f'Creating app {contextroot} {server=}' )
          self.contextroot = contextroot
          self.baseurl = urllib.parse.urljoin(server.baseurl, contextroot) + "/"
          self.jts = jts
          self.server = server
          self.project_areas_xml = None
          self._projects = None
          self.headers = {}
          self.cmServiceProviders = 'oslc_config:cmServiceProviders'
          self.iid=None # app has a dummy (empty) iid
          self.hooks = []
          self.default_query_resource = None
  
      def retrieve_cm_service_provider_xml(self):
          cm_service_provider_uri = rdfxml.xmlrdf_get_resource_uri(self.rootservices_xml,
                                                                       self.cmServiceProviders)
          rdf = self.execute_get_rdf_xml(cm_service_provider_uri, intent="Retrieve application CM Service Provider" )
          return rdf
  
      def retrieve_oslc_catalog_xml(self):
          oslccataloguri = rdfxml.xmlrdf_get_resource_uri(self.rootservices_xml, self.serviceproviders)
          if oslccataloguri is None:
              return None
          return self.execute_get_rdf_xml(oslccataloguri, intent="Retrieve application OSLC Catalog (list of projects)")
  
      # get local headers
      def _get_headers(self, headers=None):
          logger.info( f"app_gh" )
          result = {'X-Requested-With': 'XMLHttpRequest', 'Referer': self.reluri('web'),'OSLC-Core-Version':'2.0'}
          if self.headers:
              result.update(self.headers)
          result.update(self._get_oslc_headers())
          if headers:
              result.update(headers)
          logger.info( f"app_gh {result}" )
          return result
  
      # get a request with local headers
      def _get_request(self, verb, reluri='', *, params=None, headers=None, data=None):
          fullheaders = self._get_headers()
          if headers is not None:
              fullheaders.update(headers)
          sortedparams = None if params is None else {k:params[k] for k in sorted(params.keys())}
          request = httpops.HttpRequest( self.server._session, verb, self.reluri(reluri), params=sortedparams, headers=fullheaders, data=data)
          return request
  
      def _get_oslc_headers(self, headers=None):
          result = {
              'Accept': 'application/rdf+xml'
              , 'Referer': self.reluri('web')
              , 'OSLC-Core-Version': '2.0'
          }
          if headers:
              result.update(headers)
          return result
  
      def find_projectname_from_uri(self,name_or_uri):
          self._load_projects()
              
          if self.is_project_uri(name_or_uri):
              # find the project
              if name_or_uri in self._projects:
                  return self._projects[name_or_uri]['name']
          else:
              return name_or_uri
  
      def is_project_uri(self, uri):
          if uri.startswith(self.baseurl) and '/process/project-areas/' in uri:
              return True
          return False
  
      # return an absolute URL for a url relative to this app
      # NOTE if reluri has a leading / this will be relative to the serverhostname:port
      # i.e. the app context root will be removed.
      # So if you want an app-relative URL don't use a leading /
      def reluri(self, reluri=''):
          return urllib.parse.urljoin(self.baseurl,reluri)
  
      # load the projects from the project areas XML - doesn't create any project classes, this is done later when finding a project to open
      def _load_projects(self,include_archived=False,force=False):
          if self.project_class is None:
              raise Exception(f"projectClass has not been set on {self}!")
          if self._projects is not None and not force:
              return
          logger.info( "Loading projects")
          self._projects = {}
          uri = rdfxml.xmlrdf_get_resource_uri(self.rootservices_xml, 'jp06:projectAreas')
          params = {}
          if include_archived:
              params['includeArchived'] = 'true'
          self.project_areas_xml = self.execute_get_xml(uri, params=params, intent="Retrieve all project area definitions" )
          logger.debug( f"{self.project_areas_xml=}" )
          for projectel in rdfxml.xml_find_elements(self.project_areas_xml,".//jp06:project-area" ):
              logger.debug( f"{projectel=}" )
              projectu = rdfxml.xmlrdf_get_resource_text(projectel,".//jp06:url")
              projectname = rdfxml.xmlrdf_get_resource_uri(projectel,attrib='jp06:name')
              logger.debug( f"{projectname=}" )
              is_optin = False
              singlemode = False
              if self.supports_configs:
                  en = rdfxml.xmlrdf_get_resource_text(projectel,'.//jp:configuration-management-enabled')
                  is_optin = ( rdfxml.xmlrdf_get_resource_text(projectel,'.//jp:configuration-management-enabled') == "true" )
                  singlemode = ( rdfxml.xmlrdf_get_resource_text(projectel,'.//jp:configuration-management-mode') == "SINGLE" )
              logger.info( f"{projectname=} {projectu=} {is_optin=} {singlemode=}" )
  
              self._projects[projectu] = {'name':projectname, 'project': None, 'projectu': projectu, 'is_optin': is_optin, 'singlemode': singlemode }
              self._projects[projectname] = projectu
  
      # get an instance for a specific project
      def find_project(self, projectname_or_uri, include_archived=False):
          logger.info( f"Find project {projectname_or_uri}")
          self._load_projects()
          if self.is_project_uri(projectname_or_uri):
              if projectname_or_uri in self._projects:
                  res = self._projects[projectname_or_uri]['project']
                  if res is None:
                      # create the project instance
                      res = self.project_class(self._projects[projectname_or_uri]['name'], self._projects[projectname_or_uri]['projectu'], self, is_optin=self._projects[projectname_or_uri]['is_optin'],singlemode=self._projects[projectname_or_uri]['singlemode'])
              else:
                  res = None
          else:
              # must be a name
              projectu = self._projects.get(projectname_or_uri)
              print( f"{projectu=}" )
              if projectu is None:
                  res = None
              else:
                  res = self._projects[projectu]['project']
                  if res is None:
                      # create the project instance
                      res = self.project_class(self._projects[projectu]['name'], self._projects[projectu]['projectu'], self, is_optin=self._projects[projectu]['is_optin'],singlemode=self._projects[projectu]['singlemode'])
          logger.info( f'Project {projectname_or_uri} found {projectu} {res}' )
          return res
  
      def report_type_system( self ):
          qcdetails = self.get_query_capability_uris()
          report = "<HTML><BODY>\n"
          report += f"<H1>Type system report for application {self.domain}</H1>\n"
          report += "<H2>Application Queryable Resource Types, short name and URI</H2>\n"
          rows = []
          for k in sorted(qcdetails.keys()):
              shortname = k.split('#')[-1]
              shortname +=  " (default)" if self.default_query_resource is not None and k==rdfxml.tag_to_uri(self.default_query_resource) else ""
              rows.append( [shortname,k,qcdetails[k]])
          # print in a nice table with equal length columns
          report += utils.print_in_html(rows,['Short Name', 'URI', 'Query Capability URI'])
  
          rows = []
          for prefix in sorted(rdfxml.RDF_DEFAULT_PREFIX.keys()):
              rows.append([prefix,rdfxml.RDF_DEFAULT_PREFIX[prefix]] )
          report += "<H2>Prefixes</H2>\n"
          report += utils.print_in_html(rows,['Prefix', 'URI'])
          report += "</BODY></HTML>\n"
          return report
  
      def get_query_capability_uri(self,resource_type=None,context=None):
          context = context or self
          resource_type = resource_type or context.default_query_resource
          return self.get_query_capability_uri_from_xml(capabilitiesxml=context.retrieve_cm_service_provider_xml(), resource_type=resource_type,context=context)
  
      def get_query_capability_uris(self,resource_type=None,context=None):
          context = context or self
          resource_type = resource_type or context.default_query_resource
          return self.get_query_capability_uris_from_xml(capabilitiesxml=context.retrieve_cm_service_provider_xml(),context=context)
  
      def get_query_capability_uri_from_xml(self,capabilitiesxml,resource_type,context):
          logger.info( f"get_query_capability_uri_from_xml {self=} {resource_type=} {capabilitiesxml=}" )
          if resource_type is None:
              raise Exception( "You must provide a resource type" )
          # ensure we have a URI for the resource type
          resource_type_u = rdfxml.tag_to_uri(resource_type)
          # get list of [resourcetype,uri]
          qcs = self.get_query_capability_uris_from_xml(capabilitiesxml=capabilitiesxml,context=context)
          if resource_type_u.startswith( 'http' ):
              # looking for a complete precise URI
              if resource_type_u in qcs:
                  return qcs[resource_type_u]
              raise Exception( f"Resource type {resource_type} not found" )
          # didn't specify a URI - find the first match at the end of the resouce type
          for k,v in qcs.items():
              if k.endswith(resource_type):
                  return v
          raise Exception( f"Query capability {resource_type} {resource_type_u} not found!" )
  
      # returns a dictionary of resource type to query capability URI
      # this is used when the XML doesn't have references off to other URLs (like GCM does)
      def get_query_capability_uris_from_xml(self,capabilitiesxml,context):
          logger.info( f"get_query_capability_uris_from_xml {self=} {capabilitiesxml=}" )
          qcs = {}
          #<oslc:QueryCapability>
          #    <oslc:resourceType rdf:resource="http://open-services.net/ns/cm#ChangeRequest"/>
          #    <oslc:queryBase rdf:resource="https://jazz.ibm.com:9443/ccm/oslc/contexts/_2H-_4OpoEemSicvc8AFfxQ/workitems"/>
          # find a queryBase and it's the containing tag that has the info
          for qcx in rdfxml.xml_find_elements(capabilitiesxml,'.//oslc:queryBase/..'):
              for qcrtx in rdfxml.xml_find_elements( qcx, 'oslc:resourceType'):
                  qcs[rdfxml.xmlrdf_get_resource_uri(qcrtx)] = rdfxml.xmlrdf_get_resource_uri(qcx, "oslc:queryBase")
                  logger.debug( f"{rdfxml.xmlrdf_get_resource_uri(qcrtx)=}" )
          return qcs
  
      def get_factory_uri_from_xml(self,factoriesxml,resource_type,context):
          logger.info( f"get_factory_uri_from_xml {self=} {resource_type=} {factoriesxml=}" )
          if resource_type is None:
              raise Exception( "You must provide a resource type" )
          # ensure we have a URI for the resource type
          resource_type_u = rdfxml.tag_to_uri(resource_type)
          # get list of [resourcetype,uri]
          qcs = self.get_factory_uris_from_xml(factoriesxml=factoriesxml,context=context)
          if resource_type_u.startswith( 'http' ):
              # looking for a complete precise URI
              if resource_type_u in qcs:
                  return qcs[resource_type_u]
              raise Exception( f"Factory for resource type {resource_type} not found" )
          # didn't specify a URI - find the first match at the end of the resouce type
          for k,v in qcs.items():
              if k.endswith(resource_type):
                  return v
          raise Exception( f"QFactory {resource_type} {resource_type_u} not found!" )
  
      # returns a dictionary of resource type to factory URI
      # this is used when the XML doesn't have references off to other URLs (like GCM does)
      def get_factory_uris_from_xml(self,factoriesxml,context):
          logger.info( f"get_factory_uris_from_xml {self=} {factoriesxml=}" )
          qcs = {}
          #<oslc:QueryCapability>
          #    <oslc:resourceType rdf:resource="http://open-services.net/ns/cm#ChangeRequest"/>
          #    <oslc:queryBase rdf:resource="https://jazz.ibm.com:9443/ccm/oslc/contexts/_2H-_4OpoEemSicvc8AFfxQ/workitems"/>
          # find a queryBase and it's the containing tag that has the info
          for qcx in rdfxml.xml_find_elements(factoriesxml,'.//oslc:CreationFactory'):
              for qcrtx in rdfxml.xml_find_elements( qcx, 'oslc:resourceType'):
                  qcs[rdfxml.xmlrdf_get_resource_uri(qcrtx)] = rdfxml.xmlrdf_get_resource_uri(qcx, "oslc:creation")
                  logger.debug( f"{rdfxml.xmlrdf_get_resource_uri(qcrtx)=}" )
          return qcs
  
      def is_user_uri(self, uri):
          logger.info( f"{self=} {self.jts=}" )
          if uri and uri.startswith(self.jts.baseurl) and '/users/' in uri:
              return True
          return False
  
      def user_uritoname_resolver(self, uri):
          if self.is_user_uri(uri):
              res = uri[uri.rfind("/") + 1:]
              return res
          raise Exception(f"Bad user uri {uri}")
  
      def is_user_name(self, name):
          logger.info( f"Checking name {name}" )
          if not name or name.startswith( "http:") or name.startswith( "https:"):
              return False
          res = self.user_nametouri_resolver( name,raiseifinvalid=False)
          if res is not None:
              return True
          return False
  
      def user_nametouri_resolver(self, name, raiseifinvalid=True):
          logger.info( f"Converting name {name}" )
          if not raiseifinvalid or self.is_user_name(name):
              user_uri = self.jts.baseurl+f"users/{name}"
              # check it using whoami
              try:
                  res = self.execute_get(user_uri, intent="Try to retrieve User" )
              except requests.exceptions.HTTPError as e:
                  res = None
              if res:
                  return user_uri
              else:
                  if raiseifinvalid:
                      raise Exception( f"User {name} is not known on this server" )
                  return None
          raise Exception(f"Bad user  name {name}")
  
      def resolve_project_nametouri(self, name, raiseifinvalid=True):
          # find project for name
          self._load_projects()
          result = self._projects.get(name)
          logger.debug( f"resolve_project_nametouri {name} {result}" )
          return result
  
  #################################################################################################
  
  class JTSApp(_App):
      'The JTS application'
      domain = 'jts'
      project_class = None
      supports_configs = False
      supports_components = False
      supports_reportable_rest = False
  
      def __init__(self, server, contextroot, jts=None):
          super().__init__(server, contextroot, jts=self)
  
  
      def find_project(self, projectname):
          raise Exception("JTS does not have projects!")
  
  