// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.flows.domain;

import static com.google.common.collect.Iterables.concat;
import static google.registry.flows.FlowUtils.unmarshalEpp;

import com.google.common.collect.ImmutableList;
import google.registry.flows.EppException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.flows.EppException.RequiredParameterMissingException;
import google.registry.model.mark.Mark;
import google.registry.model.mark.ProtectedMark;
import google.registry.model.mark.Trademark;
import google.registry.model.smd.AbstractSignedMark;
import google.registry.model.smd.EncodedSignedMark;
import google.registry.model.smd.SignedMark;
import google.registry.model.smd.SignedMarkRevocationList;
import google.registry.tmch.TmchXmlSignature;
import google.registry.tmch.TmchXmlSignature.CertificateSignatureException;
import jakarta.inject.Inject;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.SignatureException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CertificateRevokedException;
import java.time.Instant;
import javax.xml.crypto.MarshalException;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;

/** TMCH utility functions for domain flows. */
public final class DomainFlowTmchUtils {

  private final TmchXmlSignature tmchXmlSignature;

  @Inject
  public DomainFlowTmchUtils(TmchXmlSignature tmchXmlSignature) {
    this.tmchXmlSignature = tmchXmlSignature;
  }

  public SignedMark verifySignedMarks(
      ImmutableList<AbstractSignedMark> signedMarks, String domainLabel, Instant now)
      throws EppException {
    if (signedMarks.isEmpty()) {
      throw new SignedMarksListEmptyException();
    }
    if (signedMarks.size() > 1) {
      throw new TooManySignedMarksException();
    }
    if (!(signedMarks.get(0) instanceof EncodedSignedMark encodedSignedMark)) {
      throw new SignedMarksMustBeEncodedException();
    }
    SignedMark signedMark = verifyEncodedSignedMark(encodedSignedMark, now);
    return verifySignedMarkValidForDomainLabel(signedMark, domainLabel);
  }

  public SignedMark verifySignedMarkValidForDomainLabel(SignedMark signedMark, String domainLabel)
      throws NoMarksFoundMatchingDomainException {
    if (!containsMatchingLabel(signedMark.getMark(), domainLabel)) {
      throw new NoMarksFoundMatchingDomainException();
    }
    return signedMark;
  }

  public SignedMark verifyEncodedSignedMark(EncodedSignedMark encodedSignedMark, Instant now)
      throws EppException {
    if (!"base64".equals(encodedSignedMark.getEncoding())) {
      throw new Base64RequiredForEncodedSignedMarksException();
    }
    byte[] signedMarkData;
    try {
      signedMarkData = encodedSignedMark.getBytes();
    } catch (IllegalStateException e) {
      throw new SignedMarkEncodingErrorException(e);
    }

    SignedMark signedMark;
    try {
      signedMark = unmarshalEpp(SignedMark.class, signedMarkData);
    } catch (EppException e) {
      throw new SignedMarkParsingErrorException(e);
    }

    if (SignedMarkRevocationList.get().isSmdRevoked(signedMark.getId(), now)) {
      throw new SignedMarkRevokedErrorException();
    }

    try {
      tmchXmlSignature.verify(signedMarkData);
    } catch (CertificateExpiredException e) {
      throw new SignedMarkCertificateExpiredException(e);
    } catch (CertificateNotYetValidException e) {
      throw new SignedMarkCertificateNotYetValidException(e);
    } catch (CertificateRevokedException e) {
      throw new SignedMarkCertificateRevokedException(e);
    } catch (CertificateSignatureException e) {
      throw new SignedMarkCertificateSignatureException(e);
    } catch (SignatureException | XMLSignatureException e) {
      throw new SignedMarkSignatureException(e);
    } catch (GeneralSecurityException e) {
      throw new SignedMarkCertificateInvalidException(e);
    } catch (IOException
        | MarshalException
        | SAXException
        | ParserConfigurationException e) {
      throw new SignedMarkParsingErrorException(e);
    }

    if (now.isBefore(signedMark.getCreationTime())) {
      throw new FoundMarkNotYetValidException();
    }

    if (now.isAfter(signedMark.getExpirationTime())) {
      throw new FoundMarkExpiredException();
    }

    return signedMark;
  }

  /** Returns true if the mark contains a valid claim that matches the label. */
  private static boolean containsMatchingLabel(Mark mark, String label) {
    for (Trademark trademark : mark.getTrademarks()) {
      if (trademark.getLabels().contains(label)) {
        return true;
      }
    }
    for (ProtectedMark protectedMark
        : concat(mark.getTreatyOrStatuteMarks(), mark.getCourtMarks())) {
      if (protectedMark.getLabels().contains(label)) {
        return true;
      }
    }
    return false;
  }

  /** Encoded signed marks must use base64 encoding. */
  static class Base64RequiredForEncodedSignedMarksException
      extends ParameterValuePolicyErrorException {
    public Base64RequiredForEncodedSignedMarksException() {
      super("Encoded signed marks must use base64 encoding");
    }
  }

  /** The provided mark does not match the desired domain label. */
  public static class NoMarksFoundMatchingDomainException
      extends RequiredParameterMissingException {
    public NoMarksFoundMatchingDomainException() {
      super("The provided mark does not match the desired domain label");
    }
  }

  /** The provided mark is not yet valid. */
  static class FoundMarkNotYetValidException extends ParameterValuePolicyErrorException {
    public FoundMarkNotYetValidException() {
      super("The provided mark is not yet valid");
    }
  }

  /** The provided mark has expired. */
  static class FoundMarkExpiredException extends ParameterValuePolicyErrorException {
    public FoundMarkExpiredException() {
      super("The provided mark has expired");
    }
  }

  /** Certificate used in signed mark signature was revoked by ICANN. */
  static class SignedMarkCertificateRevokedException extends ParameterValuePolicyErrorException {
    public SignedMarkCertificateRevokedException() {
      super("Signed mark certificate was revoked");
    }

    public SignedMarkCertificateRevokedException(Throwable cause) {
      this();
      initCause(cause);
    }
  }

  /** Certificate used in signed mark signature has expired. */
  static class SignedMarkCertificateNotYetValidException
      extends ParameterValuePolicyErrorException {
    public SignedMarkCertificateNotYetValidException() {
      super("Signed mark certificate not yet valid");
    }

    public SignedMarkCertificateNotYetValidException(Throwable cause) {
      this();
      initCause(cause);
    }
  }

  /** Certificate used in signed mark signature has expired. */
  static class SignedMarkCertificateExpiredException extends ParameterValuePolicyErrorException {
    public SignedMarkCertificateExpiredException() {
      super("Signed mark certificate has expired");
    }

    public SignedMarkCertificateExpiredException(Throwable cause) {
      this();
      initCause(cause);
    }
  }

  /** Certificate parsing error, or possibly a bad provider or algorithm. */
  static class SignedMarkCertificateInvalidException extends ParameterValuePolicyErrorException {
    public SignedMarkCertificateInvalidException() {
      super("Signed mark certificate is invalid");
    }

    public SignedMarkCertificateInvalidException(Throwable cause) {
      this();
      initCause(cause);
    }
  }

  /** Invalid signature on a signed mark. */
  static class SignedMarkCertificateSignatureException extends ParameterValuePolicyErrorException {
    public SignedMarkCertificateSignatureException() {
      super("Signed mark certificate not signed by ICANN");
    }

    public SignedMarkCertificateSignatureException(Throwable cause) {
      this();
      initCause(cause);
    }
  }

  /** Invalid signature on a signed mark. */
  static class SignedMarkSignatureException extends ParameterValuePolicyErrorException {
    public SignedMarkSignatureException() {
      super("Signed mark signature is invalid");
    }

    public SignedMarkSignatureException(Throwable cause) {
      this();
      initCause(cause);
    }
  }

  /** Signed marks must be encoded. */
  static class SignedMarksMustBeEncodedException extends ParameterValuePolicyErrorException {
    public SignedMarksMustBeEncodedException() {
      super("Signed marks must be encoded");
    }
  }

  /** Signed marks list cannot be empty. */
  static class SignedMarksListEmptyException extends RequiredParameterMissingException {
    public SignedMarksListEmptyException() {
      super("Signed marks list cannot be empty");
    }
  }

  /** Only one signed mark is allowed per application. */
  static class TooManySignedMarksException extends ParameterValuePolicyErrorException {
    public TooManySignedMarksException() {
      super("Only one signed mark is allowed per application");
    }
  }

  /** Signed mark data is revoked. */
  static class SignedMarkRevokedErrorException extends ParameterValuePolicyErrorException {
    public SignedMarkRevokedErrorException() {
      super("SMD has been revoked");
    }
  }

  /** Error while parsing encoded signed mark data. */
  static class SignedMarkParsingErrorException extends ParameterValueSyntaxErrorException {
    public SignedMarkParsingErrorException() {
      super("Error while parsing encoded signed mark data");
    }

    public SignedMarkParsingErrorException(Throwable cause) {
      this();
      initCause(cause);
    }
  }

  /** Signed mark data is improperly encoded. */
  static class SignedMarkEncodingErrorException extends ParameterValueSyntaxErrorException {
    public SignedMarkEncodingErrorException() {
      super("Signed mark data is improperly encoded");
    }

    public SignedMarkEncodingErrorException(Throwable cause) {
      this();
      initCause(cause);
    }
  }

}
