/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.eclipse.aether.internal.impl.filter.prefixes;

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.aether.repository.RemoteRepository;

import static java.util.Objects.requireNonNull;

/**
 * Prefixes source and parser.
 * <p>
 * This class is "clean room" reimplementation of
 * <a href="https://github.com/sonatype/nexus-public/blob/daf1e9c2844282132063f1d8bad914c93efa3d0e/components/nexus-core/src/main/java/org/sonatype/nexus/proxy/maven/routing/internal/TextFilePrefixSourceMarshaller.java">original class</a>.
 *
 * @since 2.0.11
 */
public interface PrefixesSource {
    /**
     * The origin repository of this source.
     */
    RemoteRepository origin();

    /**
     * The file path (ie local repository or user provided one) this source got entries from.
     */
    Path path();

    /**
     * Message worth logging if {@link #valid()} returns {@code false}.
     */
    String message();

    /**
     * Returns {@code true} if source is valid and contains valid entries.
     */
    boolean valid();

    /**
     * The prefix entries.
     */
    List<String> entries();

    /**
     * Creates {@link PrefixesSource} out of passed in parameters, never returns {@code null}. The returned
     * source should be checked for {@link #valid()} and use only if it returns {@code true}.
     * <p>
     * This method is "forgiving" to all kind of IO problems while reading (file not found, etc.) and will never
     * throw {@link IOException} as prefix file processing should not interrupt main flow due which prefix file
     * processing is happening in the first place. Ideally, user is notified at least by logging if any problem happens.
     */
    static PrefixesSource of(RemoteRepository origin, Path path) {
        requireNonNull(origin, "origin is null");
        requireNonNull(path, "path is null");
        return new Parser(origin, path).parse();
    }

    final class Parser {
        private static final String PREFIX_MAGIC = "## repository-prefixes/2.0";
        private static final String PREFIX_LEGACY_MAGIC = "# Prefix file generated by Sonatype Nexus";
        private static final String PREFIX_UNSUPPORTED = "@ unsupported";
        private static final int MAX_ENTRIES = 100_000;
        private static final int MAX_LINE_LENGTH = 250;

        private final RemoteRepository origin;
        private final Path path;

        private Parser(RemoteRepository origin, Path path) {
            this.origin = origin;
            this.path = path;
        }

        private PrefixesSource parse() {
            try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
                ArrayList<String> entries = new ArrayList<>();
                String line = reader.readLine();
                if (!PREFIX_MAGIC.equals(line)
                        && !PREFIX_LEGACY_MAGIC.equals(line)
                        && !PREFIX_UNSUPPORTED.equals(line)) {
                    return invalid(origin, path, "No expected magic in file");
                }
                while (line != null) {
                    line = line.trim();
                    if (PREFIX_UNSUPPORTED.equals(line)) {
                        // abort; if file contains this line anywhere is unsupported
                        return invalid(origin, path, "Declares itself unsupported");
                    }
                    if (!line.startsWith("#") && !line.isEmpty()) {
                        // entry length
                        if (line.length() > MAX_LINE_LENGTH) {
                            return invalid(origin, path, "Contains too long line");
                        }
                        // entry should be ASCII subset
                        if (!line.chars().allMatch(c -> c < 128)) {
                            return invalid(origin, path, "Contains non-ASCII characters");
                        }
                        // entry should be actually a bit less than ASCII, filtering most certain characters
                        if (line.contains(":")
                                || line.contains("<")
                                || line.contains(">")
                                || line.contains("\\")
                                || line.contains("//")) {
                            return invalid(origin, path, "Contains forbidden characters");
                        }

                        // strip leading dot if needed (ie manually crafted file using UN*X find command)
                        while (line.startsWith(".")) {
                            line = line.substring(1);
                        }
                        entries.add(line);
                    }
                    line = reader.readLine();

                    // dump big files
                    if (entries.size() > MAX_ENTRIES) {
                        return invalid(origin, path, "Contains too many entries");
                    }
                }
                return new Impl(origin, path, "OK", true, Collections.unmodifiableList(entries));
            } catch (NoSuchFileException e) {
                return invalid(origin, path, "No such file");
            } catch (IOException e) {
                return invalid(origin, path, "Could not read the file: " + e.getMessage());
            }
        }

        private static PrefixesSource invalid(RemoteRepository origin, Path path, String message) {
            return new Impl(origin, path, message, false, Collections.emptyList());
        }

        private static class Impl implements PrefixesSource {
            private final RemoteRepository origin;
            private final Path path;
            private final String message;
            private final boolean valid;
            private final List<String> entries;

            private Impl(RemoteRepository origin, Path path, String message, boolean valid, List<String> entries) {
                this.origin = requireNonNull(origin);
                this.path = requireNonNull(path);
                this.message = message;
                this.valid = valid;
                this.entries = entries;
            }

            @Override
            public RemoteRepository origin() {
                return origin;
            }

            @Override
            public Path path() {
                return path;
            }

            @Override
            public String message() {
                return message;
            }

            @Override
            public boolean valid() {
                return valid;
            }

            @Override
            public List<String> entries() {
                return entries;
            }
        }
    }
}
