/*
 * 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.apache.cassandra.audit;

import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.UUID;

import javax.annotation.Nullable;

import com.google.common.annotations.VisibleForTesting;

import org.apache.commons.lang3.StringUtils;

import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.utils.FBUtilities;

import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;

public class AuditLogEntry
{
    static final String DEFAULT_KEY_VALUE_SEPARATOR = ":";
    static final String DEFAULT_FIELD_SEPARATOR = "|";
    private final InetAddressAndPort host = FBUtilities.getBroadcastAddressAndPort();
    private final InetAddressAndPort source;
    private final String user;
    private final long timestamp;
    private final AuditLogEntryType type;
    private final UUID batch;
    private final String keyspace;
    private final String scope;
    private final String operation;
    private final QueryOptions options;
    private final QueryState state;
    private final Map<String, Object> metadata;

    private AuditLogEntry(Builder builder)
    {
        this.type = builder.type;
        this.source = builder.source;
        this.user = builder.user;
        this.timestamp = builder.timestamp;
        this.batch = builder.batch;
        this.keyspace = builder.keyspace;
        this.scope = builder.scope;
        this.operation = builder.operation;
        this.options = builder.options;
        this.state = builder.state;
        this.metadata = builder.metadata;
    }

    @VisibleForTesting
    public String getLogString()
    {
        return getLogString(DEFAULT_KEY_VALUE_SEPARATOR, DEFAULT_FIELD_SEPARATOR);
    }

    String getLogString(String keyValueSeparator, String fieldSeparator)
    {
        StringBuilder builder = new StringBuilder(100);
        builder.append("user").append(keyValueSeparator).append(user)
               .append(fieldSeparator).append("host").append(keyValueSeparator).append(host);

        // Source is only expected to be null during testing
        // in MacOS when running in-jvm dtests
        if (source != null)
        {
            builder.append(fieldSeparator).append("source").append(keyValueSeparator).append(source.getAddress());
            if (source.getPort() > 0)
            {
                builder.append(fieldSeparator).append("port").append(keyValueSeparator).append(source.getPort());
            }
        }

        builder.append(fieldSeparator).append("timestamp").append(keyValueSeparator).append(timestamp)
               .append(fieldSeparator).append("type").append(keyValueSeparator).append(type)
               .append(fieldSeparator).append("category").append(keyValueSeparator).append(type.getCategory());

        if (batch != null)
        {
            builder.append(fieldSeparator).append("batch").append(keyValueSeparator).append(batch);
        }
        if (StringUtils.isNotBlank(keyspace))
        {
            builder.append(fieldSeparator).append("ks").append(keyValueSeparator).append(keyspace);
        }
        if (StringUtils.isNotBlank(scope))
        {
            builder.append(fieldSeparator).append("scope").append(keyValueSeparator).append(scope);
        }
        if (StringUtils.isNotBlank(operation))
        {
            builder.append(fieldSeparator).append("operation").append(keyValueSeparator).append(operation);
        }
        if (metadata != null && !metadata.isEmpty())
        {
            metadata.forEach((key, value) -> builder.append(fieldSeparator).append(key).append(keyValueSeparator).append(value));
        }
        return builder.toString();
    }

    public InetAddressAndPort getHost()
    {
        return host;
    }

    public InetAddressAndPort getSource()
    {
        return source;
    }

    public String getUser()
    {
        return user;
    }

    public long getTimestamp()
    {
        return timestamp;
    }

    public AuditLogEntryType getType()
    {
        return type;
    }

    public UUID getBatch()
    {
        return batch;
    }

    public String getKeyspace()
    {
        return keyspace;
    }

    public String getScope()
    {
        return scope;
    }

    public String getOperation()
    {
        return operation;
    }

    public QueryOptions getOptions()
    {
        return options;
    }

    public QueryState getState()
    {
        return state;
    }

    public static class Builder
    {
        private static final InetAddressAndPort DEFAULT_SOURCE;

        static
        {
            try
            {
                DEFAULT_SOURCE = InetAddressAndPort.getByNameOverrideDefaults("0.0.0.0", 0);
            }
            catch (UnknownHostException e)
            {

                throw new RuntimeException("failed to create default source address", e);
            }
        }

        private static final String DEFAULT_OPERATION = StringUtils.EMPTY;

        private AuditLogEntryType type;
        private InetAddressAndPort source;
        private String user;
        private long timestamp;
        private UUID batch;
        private String keyspace;
        private String scope;
        private String operation;
        private QueryOptions options;
        private QueryState state;
        private Map<String, Object> metadata;

        public Builder(QueryState queryState)
        {
            state = queryState;

            ClientState clientState = queryState.getClientState();

            if (clientState != null)
            {
                InetSocketAddress addr = clientState.getRemoteAddress();
                if (addr != null)
                {
                    source = InetAddressAndPort.getByAddressOverrideDefaults(addr.getAddress(), addr.getPort());
                }

                AuthenticatedUser authenticatedUser = clientState.getUser();
                if (authenticatedUser != null)
                {
                    user = authenticatedUser.getName();

                    if (authenticatedUser.getMetadata() != null)
                    {
                        metadata = Map.copyOf(authenticatedUser.getMetadata());
                    }
                }
                keyspace = clientState.getRawKeyspace();
            }
            else
            {
                source = DEFAULT_SOURCE;
                user = AuthenticatedUser.SYSTEM_USER.getName();
            }

            timestamp = currentTimeMillis();
        }

        public Builder(AuditLogEntry entry)
        {
            type = entry.type;
            source = entry.source;
            user = entry.user;
            timestamp = entry.timestamp;
            batch = entry.batch;
            keyspace = entry.keyspace;
            scope = entry.scope;
            operation = entry.operation;
            options = entry.options;
            state = entry.state;
            metadata = entry.metadata != null ? Map.copyOf(entry.metadata) : null;
        }

        public Builder setType(AuditLogEntryType type)
        {
            this.type = type;
            return this;
        }

        public Builder(AuditLogEntryType type)
        {
            this.type = type;
            operation = DEFAULT_OPERATION;
        }

        public Builder setUser(String user)
        {
            this.user = user;
            return this;
        }

        public Builder setBatch(UUID batch)
        {
            this.batch = batch;
            return this;
        }

        public Builder setTimestamp(long timestampMillis)
        {
            this.timestamp = timestampMillis;
            return this;
        }

        public Builder setKeyspace(QueryState queryState, @Nullable CQLStatement statement)
        {
            keyspace = statement != null && statement.getAuditLogContext().keyspace != null
                       ? statement.getAuditLogContext().keyspace
                       : queryState.getClientState().getRawKeyspace();
            return this;
        }

        public Builder setKeyspace(String keyspace)
        {
            this.keyspace = keyspace;
            return this;
        }

        public Builder setKeyspace(CQLStatement statement)
        {
            this.keyspace = statement.getAuditLogContext().keyspace;
            return this;
        }

        public Builder setScope(CQLStatement statement)
        {
            this.scope = statement.getAuditLogContext().scope;
            return this;
        }

        public Builder setOperation(String operation)
        {
            this.operation = operation;
            return this;
        }

        public void appendToOperation(String str)
        {
            if (StringUtils.isNotBlank(str))
            {
                if (operation.isEmpty())
                    operation = str;
                else
                    operation = operation.concat("; ").concat(str);
            }
        }

        public Builder setOptions(QueryOptions options)
        {
            this.options = options;
            return this;
        }

        public Builder setMetadata(Map<String, Object> metadata)
        {
            this.metadata = metadata != null ? Map.copyOf(metadata) : null;
            return this;
        }

        public AuditLogEntry build()
        {
            timestamp = timestamp > 0 ? timestamp : currentTimeMillis();
            return new AuditLogEntry(this);
        }
    }
}

