// Copyright 2020 The Bazel 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 com.google.devtools.build.lib.authandtls;

import static com.google.common.base.Predicates.not;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.util.Objects.requireNonNull;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.authandtls.Netrc.Credential;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;

/**
 * A parser used to parse .netrc content.
 *
 * @see <a href="https://man.cx/netrc(4)">netrc − file for ftp remote login data</a>
 * @see <a
 *     href="https://github.com/bazelbuild/bazel/blob/master/tools/build_defs/repo/utils.bzl#L203-L204">Starlark
 *     netrc parser</a>
 */
public class NetrcParser {
  private static final String MACHINE = "machine";
  private static final String MACDEF = "macdef";
  private static final String DEFAULT = "default";
  private static final String LOGIN = "login";
  private static final String PASSWORD = "password";
  private static final String ACCOUNT = "account";

  interface Token {}

  record ItemToken(String item) implements Token {
    ItemToken {
      requireNonNull(item, "item");
    }

    public static ItemToken create(String item) {
      return new ItemToken(item);
    }

  }

  record NewlineToken() implements Token {
    public static NewlineToken create() {
      return new NewlineToken();
    }
  }

  record CommentToken() implements Token {
    public static CommentToken create() {
      return new CommentToken();
    }
  }

  private static class TokenStream implements Closeable {
    private final BufferedReader bufferedReader;
    private final Queue<Token> tokens = new ArrayDeque<>();

    TokenStream(InputStream inputStream) throws IOException {
      bufferedReader = new BufferedReader(new InputStreamReader(inputStream, ISO_8859_1));
      processLine();
    }

    @Override
    public void close() throws IOException {
      bufferedReader.close();
    }

    private void processLine() throws IOException {
      String line = bufferedReader.readLine();
      if (line == null) {
        return;
      }

      // Comments start with #
      if (line.startsWith("#")) {
        tokens.add(CommentToken.create());
      } else {
        Arrays.stream(line.split("\\s+"))
            .filter(not(Strings::isNullOrEmpty))
            .map(ItemToken::create)
            .forEach(tokens::add);
      }

      tokens.add(NewlineToken.create());
    }

    public boolean hasNext() {
      return !tokens.isEmpty();
    }

    public Token next() throws IOException {
      Token token = tokens.poll();
      if (tokens.isEmpty()) {
        processLine();
      }
      return token;
    }

    public Token peek() {
      return tokens.peek();
    }
  }

  private NetrcParser() {}

  public static Netrc parseAndClose(InputStream inputStream) throws IOException {
    try (TokenStream tokenStream = new TokenStream(inputStream)) {
      return parse(tokenStream);
    }
  }

  private static Netrc parse(TokenStream tokenStream) throws IOException {
    Credential defaultCredential = null;
    Map<String, Credential> credentialMap = new HashMap<>();

    boolean done = false;
    while (!done && tokenStream.hasNext()) {
      Token token = tokenStream.next();
      if (token instanceof ItemToken itemToken) {
        String item = itemToken.item();
        switch (item) {
          case MACHINE -> {
            String machine = nextItem(tokenStream);
            Credential credential = parseCredentialForMachine(tokenStream, machine);
            credentialMap.put(machine, credential);
          }
          case MACDEF -> skipMacdef(tokenStream);
          case DEFAULT -> {
            defaultCredential = parseCredentialForMachine(tokenStream, DEFAULT);
            // There can be only one default token, and it must be after all machine tokens.
            done = true;
          }
          default ->
              throw new IOException(
                  String.format(
                      "Unexpected token: %s (expecting %s, %s or %s)",
                      item, MACHINE, MACDEF, DEFAULT));
        }
      }
    }

    return Netrc.create(defaultCredential, ImmutableMap.copyOf(credentialMap));
  }

  private static String nextItem(TokenStream tokenStream) throws IOException {
    while (tokenStream.hasNext()) {
      Token token = tokenStream.next();
      if (token instanceof ItemToken itemToken) {
        return itemToken.item();
      }
    }

    throw new IOException("Unexpected EOF");
  }

  /** Parse credentials for a given machine from token stream. */
  private static Credential parseCredentialForMachine(TokenStream tokenStream, String machine)
      throws IOException {
    Credential.Builder builder = Credential.builder(machine);

    boolean done = false;
    while (!done && tokenStream.hasNext()) {
      // Peek rather than taking next token since we probably won't process it
      Token token = tokenStream.peek();
      if (token instanceof ItemToken itemToken) {
        switch (itemToken.item()) {
          case LOGIN -> {
            tokenStream.next();
            builder.setLogin(nextItem(tokenStream));
          }
          case PASSWORD -> {
            tokenStream.next();
            builder.setPassword(nextItem(tokenStream));
          }
          case ACCOUNT -> {
            tokenStream.next();
            builder.setAccount(nextItem(tokenStream));
          }
          case MACHINE, MACDEF, DEFAULT -> done = true;
          default ->
              throw new IOException(
                  String.format(
                      "Unexpected item: %s (expecting %s, %s, %s, %s, %s or %s)",
                      itemToken.item(), LOGIN, PASSWORD, ACCOUNT, MACHINE, MACDEF, DEFAULT));
        }
      } else {
        tokenStream.next();
      }
    }

    return builder.build();
  }

  /** Skip macdef section since we don't need that data currently. */
  private static void skipMacdef(TokenStream tokenStream) throws IOException {
    int numNewlines = 0;
    while (tokenStream.hasNext()) {
      Token token = tokenStream.next();
      if (token instanceof NewlineToken) {
        ++numNewlines;
      } else {
        numNewlines = 0;
      }
      if (numNewlines >= 2) {
        break;
      }
    }
  }
}
