import java.util.regex.Matcher

/**
 * Pipeline to generate and to publish the documentation of a new version of Silverpeas. This
 * pipeline should be triggered once the release of Silverpeas has been successfully performed as it
 * will fetch the release report for additional required information.
 *
 * This pipeline requires the following job 'Silverpeas Project Web Site Publisher' to run the
 * corresponding pipeline in order to publish the Silverpeas community web site.
 *
 * This pipeline requires the following parameters:
 * SILVERPEAS_VERSION   the version of Silverpeas for which the documentation has to be generated
 *                      and published.
 *
 * The build is performed within a dedicated Docker image in order to ensure the reproducibility of
 * the builds and to containerize them from the host OS.
 */

String imageVersion = getDockerImageVersion()

def projects = [
    core        : 'Silverpeas-Core',
    components  : 'Silverpeas-Components']

pipeline {

  agent {
    docker {
      image "silverpeas/silverbuild:${imageVersion}"
      args '''
          -v $HOME/.m2/settings.xml:/home/silverbuild/.m2/settings.xml 
          -v $HOME/.m2/settings-security.xml:/home/silverbuild/.m2/settings-security.xml 
          -v $HOME/.gitconfig:/home/silverbuild/.gitconfig 
          -v $HOME/.ssh:/home/silverbuild/.ssh 
          -v $HOME/.gnupg:/home/silverbuild/.gnupg
          '''
    }
  }

  parameters {
    string (
        description: 'the version of Silverpeas for which the documentation will be generated',
        name: 'SILVERPEAS_VERSION'
    )
  }

  environment {
    gitBaseRepo = 'https://github.com/Silverpeas/'
    gitCredential = 'cacc0467-7c85-41d1-bf4e-eaa470dd5e59'
    mavenRepo = 'https://nexus3.silverpeas.org/repository/silverpeas'
    releaseBranch = ''
  }

  stages {
    stage('Check version of Silverpeas') {
      when {
        expression { !isStableVersion() }
      }
      steps {
        error("Documentation is published only for a stable version. Version here is: ${params.SILVERPEAS_VERSION}")
      }
    }

    stage('Prepare the publishing') {
      steps {
        copyArtifacts projectName: "Silverpeas_Release", flatten: true,
            selector: specific(params.SILVERPEAS_VERSION)
        script {
          def report = readYaml file: 'release.yaml'
          releaseBranch = report.branch
        }
        sh 'rm -f release.yaml'
      }
    }

    stage('Publish documentation of Silverpeas Core') {
      when {
        expression { releaseBranch == 'master' }
      }
      steps {
        dir(projects.core) {
          git credentialsId: gitCredential, url: (gitBaseRepo + projects.core)
          sh """
            git checkout ${params.SILVERPEAS_VERSION}
            mvn site-deploy -Pdeployment -Djava.awt.headless=true -Dmaven.test.skip=true
            """
          // the documentation of the REST API is produced by the default lifecycle: the resolve
          // goal of Swagger is bound to the compile phase and forks no compilation, hence the
          // site lifecycle alone would publish a page without any specification
          sh '''
            mvn package site-deploy -Pdeployment,restapi -pl core-restapi \
                -Djava.awt.headless=true -Dmaven.test.skip=true
            '''
          checkRestApiDoc('core-restapi/target/openapi/openapi.json', 150)
        }
      }
    }

    stage('Publish documentation of Silverpeas Components') {
      when {
        expression { releaseBranch == 'master' }
      }
      steps {
        dir(projects.components) {
          git credentialsId: gitCredential, url: (gitBaseRepo + projects.components)
          sh """
            git checkout ${params.SILVERPEAS_VERSION}
            mvn site-deploy -Pdeployment -Djava.awt.headless=true -Dmaven.test.skip=true
            """
          sh '''
            mvn package site-deploy -Pdeployment,restapi -pl components-restapi \
                -Djava.awt.headless=true -Dmaven.test.skip=true
            '''
          checkRestApiDoc('components-restapi/target/openapi/openapi.json', 60)
        }
      }
    }

    stage('Check the consistency of the REST API documentations') {
      when {
        expression { releaseBranch == 'master' }
      }
      steps {
        checkCommonResponses(
            "${projects.core}/core-restapi/target/openapi/openapi.json",
            "${projects.components}/components-restapi/target/openapi/openapi.json")
      }
    }

    stage('Publish Silverpeas Community Web Site') {
      steps {
        build job: 'Silverpeas Project Web Site Publisher', parameters: [
            string(name: 'SILVERPEAS_VERSION', value: params.SILVERPEAS_VERSION),
        ], wait: true
      }
    }
  }

  post {
    success {
      script {
        currentBuild.displayName = params.SILVERPEAS_VERSION
      }
    }
    always {
      step([$class                  : 'Mailer',
            notifyEveryUnstableBuild: true,
            recipients              : "david.lesimple@silverpeas.org, sebastien.vuillet@silverpeas.org, aurore.allibe@silverpeas.org, miguel.moquillon@silverpeas.org, silveryocha@chastagnier.com",
            sendToIndividuals       : true])
    }
  }
}

String getBranch() {
  Matcher matcher = params.SILVERPEAS_VERSION =~ '^(\\d+.\\d+)\\..*$'
  matcher ? "${matcher[0][1]}.x" : 'master'
}

boolean isStableVersion() {
  Matcher m = params.SILVERPEAS_VERSION =~ '^\\d+.\\d+(.\\d+)?$'
  return m.matches()
}

String getDockerImageVersion() {
  Matcher matcher = params.SILVERPEAS_VERSION =~ '^(\\d+.\\d+)\\..*$'
  matcher ? "${matcher[0][1]}" : 'latest'
}

/**
 * Checks the OpenAPI document that has just been generated actually documents the REST API. The
 * generation of such a document is silent about its own failures: it reports a success even when
 * it produces nothing. Without this check, an incomplete documentation would be published unnoticed.
 * <p>
 * Both the paths and the info section are checked. The latter is required by OpenAPI, and a
 * renderer refuses to display a document that misses it, whatever the quality of the rest.
 * @param document the path of the generated OpenAPI document.
 * @param expectedPathCount the minimal count of paths the document is expected to declare.
 */
void checkRestApiDoc(String document, int expectedPathCount) {
  sh """
    python3 - <<'EOF'
import json, sys
specification = json.load(open('${document}'))
paths = specification.get('paths', {})
info = specification.get('info') or {}
operations = sum(len(o) for o in paths.values())
summarized = sum(1 for p in paths.values() for o in p.values() if o.get('summary'))
print(f'REST API documentation: {len(paths)} paths, {operations} operations, '
      f'{summarized} of them summarized')
if not info.get('title') or not info.get('version'):
    sys.exit('FAILURE: the specification declares no title nor version in its info section, '
             'which OpenAPI requires and without which a renderer displays nothing')
if len(paths) < ${expectedPathCount}:
    sys.exit(f'FAILURE: only {len(paths)} paths documented, less than the expected ${expectedPathCount}')
if summarized < operations:
    sys.exit(f'FAILURE: {operations - summarized} operations without any summary, which betrays '
             f'a scan of artefacts built out of the documented branch')
EOF
    """
}

/**
 * Checks the responses that are common to all the endpoints, whatever the web resource they belong
 * to, are documented the same way in all the specifications. Such a response is brought by the
 * CommonResponsesFilter class, which core-rs provides to every project documenting its REST API.
 * Each specification must therefore declare it, and word it the same way: two of them disagreeing
 * would betray a project documented against another version of that class than the one of the
 * branch being released.
 * @param documents the paths of the generated OpenAPI documents to compare.
 */
void checkCommonResponses(String... documents) {
  sh """
    python3 - ${documents.join(' ')} <<'EOF'
import json, sys

CODE = '503'
METHODS = ('get', 'put', 'post', 'delete', 'patch', 'head', 'options')
wordings = {}
missing = []
for path in sys.argv[1:]:
    for uri, item in json.load(open(path)).get('paths', {}).items():
        for method, operation in item.items():
            if method not in METHODS:
                continue
            response = operation.get('responses', {}).get(CODE)
            if response is None:
                missing.append(f'{method.upper()} {uri} of {path}')
            else:
                wordings.setdefault(response.get('description'), set()).add(path)

if missing:
    sys.exit(f'FAILURE: {len(missing)} operations without any {CODE} response, the first one '
             f'being {missing[0]}')
if len(wordings) > 1:
    for wording, documents in wordings.items():
        print(f'  {sorted(documents)}: {wording}')
    sys.exit(f'FAILURE: the {CODE} response is worded in {len(wordings)} different ways')
print(f'Common {CODE} response, consistent across the specifications: {next(iter(wordings))}')
EOF
    """
}