/*
 * 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.jena.fuseki.servlets;

import static org.apache.jena.query.TxnType.READ;
import static org.apache.jena.query.TxnType.READ_PROMOTE;
import static org.apache.jena.query.TxnType.WRITE;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.jena.atlas.logging.Log;
import org.apache.jena.fuseki.Fuseki;
import org.apache.jena.fuseki.FusekiException;
import org.apache.jena.fuseki.server.*;
import org.apache.jena.fuseki.system.ActionCategory;
import org.apache.jena.query.TxnType;
import org.apache.jena.sparql.SystemARQ;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.sparql.core.Transactional;
import org.apache.jena.sparql.core.TransactionalLock;
import org.apache.jena.sparql.util.Context;
import org.slf4j.Logger;

/**
 * HTTP action that represents the user request lifecycle. It is is handled in the
 * {@link ActionBase#executeLifecycle(HttpAction)} method.
 */
public class HttpAction
{
    public final long id;
    public final boolean verbose;
    public final ActionCategory category;
    public final Logger log;

    // ----
    // Worth subclassing? Given this is allocated in the general lifecycle
    // it would mean there are downcasts to the specific type.

    // -- Valid only for operational actions (e.g. SPARQL).

    public  Endpoint        endpoint        = null;
    private Transactional   transactional   = null;
    private boolean         isTransactional = false;
    private DatasetGraph    activeDSG       = null;        // Set when inside begin/end.

    // -- Valid only for administration actions.

    // -- Shared items (but exact meaning may differ)
    /** Handle to dataset+services being acted on (maybe null) */
    private DataAccessPoint dataAccessPoint = null;
    private DataService dataService         = null;
    private String datasetName              = null;        // Dataset URI used (e.g. registry)
    private DatasetGraph dsg                = null;
    private Context context                 = null;

    // ----

    private boolean startTimeIsSet = false;
    private boolean finishTimeIsSet = false;

    private long startTime = -2;
    private long finishTime = -2;

    // Outcome.
    public int statusCode = -1;
    public String message = null;
    public int responseContentLength = -1;
    public String responseContentType = null;

    // Cleared to archive:
    public Map <String, String> headers = new HashMap<>();
    public HttpServletRequest request;
    public HttpServletResponseTracker response;
    private final String actionURI;
    private final String contextPath;

    private final OperationRegistry serviceDispatchRegistry;
    private final DataAccessPointRegistry dataAccessPointRegistry;

    /**
     * Creates a new HTTP Action, using the HTTP request and response, and a given ID.
     *
     * @param id given ID
     * @param log Logger for this action
     * @param request HTTP request
     * @param response HTTP response
     */
    public HttpAction(long id, Logger log, ActionCategory category, HttpServletRequest request, HttpServletResponse response) {
        this.id = id;
        // Currently server-wide, e.g. from the command line.
        this.verbose = Fuseki.getVerbose(request.getServletContext());
        this.log = log;
        this.category = category;
        this.request = request;
        this.response = new HttpServletResponseTracker(this, response);
        this.contextPath = request.getServletContext().getContextPath();
        this.actionURI = ActionLib.actionURI(request);
        this.serviceDispatchRegistry = OperationRegistry.get(request.getServletContext());
        this.dataAccessPointRegistry = DataAccessPointRegistry.get(request.getServletContext());
    }

    /**
     * Initialization after action creation, during lifecycle setup. This is "set
     * once" (in other words, constructor-like but delayed because the information is
     * not yet available at the point we want to create the HttpAction).
     *
     * This method sets the action dataset for service requests. Does not apply to "admin" and
     * "ctl" servlets. Setting will replace any existing {@link DataAccessPoint} and
     * {@link DataService}, as the {@link DatasetGraph} of the current HTTP Action.
     * </p>
     * <p>
     * Once it has updated its members, the HTTP Action will change its transactional
     * state and {@link Transactional} instance according to its base dataset graph.
     * </p>
     *
     * @param dataAccessPoint {@link DataAccessPoint}
     * @param dService {@link DataService}
     * @see Transactional
     */

    public void setRequest(DataAccessPoint dataAccessPoint, DataService dService) {
        if ( this.dataAccessPoint != null )
            throw new FusekiException("Redefinition of DataAccessPoint in the request action");
        this.dataAccessPoint = dataAccessPoint;
        if ( dataAccessPoint != null )
            this.datasetName = dataAccessPoint.getName();
        if ( this.dataService != null )
            throw new FusekiException("Redefinition of DatasetRef in the request action");
        if ( dService == null || dService.getDataset() == null )
            throw new FusekiException("Null DataService in the request action");
        this.dataService = dService;
        setDataset(dService.getDataset());
    }

    /** Minimum initialization using just a dataset.
     * <p>
     * the HTTP Action will change its transactional state and
     * {@link Transactional} instance according to its base dataset graph.
     * </p>
     * <p>There is no associated DataAccessPoint or DataService set by this operation.</p>
     *
     * @param dsg DatasetGraph
     */
    private void setDataset(DatasetGraph dsg) {
        this.dsg = dsg;
        this.context = Context.mergeCopy(Fuseki.getContext(), dsg.getContext());
        if ( dsg == null )
            return;
        setTransactionalPolicy(dsg);
    }

    private void setTransactionalPolicy(DatasetGraph dsg) {
        if ( dsg.supportsTransactionAbort() ) {
            // Use transactional if it looks safe - abort is necessary.
            transactional = dsg;
            isTransactional = true;
        } else if ( dsg.supportsTransactions() ) {
            // No abort - e.g. loading data needs buffering against syntax errors.
            transactional = dsg;
            isTransactional = false;
        } else {
            // Nothing to build on.  Be safe.
            transactional = TransactionalLock.createMutex();
            isTransactional = false;
        }
    }

    /** Return the dataset, if any (may be null) */
    public DatasetGraph getDataset() {
        return dsg;
    }

    /** Return the Context for this {@code HttpAction}. */
    public Context getContext() {
        return context;
    }

    /** Return the authenticated user this {@code HttpAction}.
     * Return null for no authenticated user.
     */
    public String getUser() {
        if ( request == null )
            return null;
        return request.getRemoteUser();
        //Same as: return request.getUserPrincipal().getName();
    }

    /**
     * Return the "Transactional" for this HttpAction.
     */

    public Transactional getTransactional() {
        return transactional;
    }

    /** This is the requestURI with the context path removed.
     *  It should be used internally for dispatch.
     */
    public String getActionURI() {
        return actionURI;
    }

    /** Get the context path.
     */
    public String getContextPath() {
        return contextPath;
    }

    /**
     * Get the {@link OperationRegistry} for this action.
     */
    public OperationRegistry getOperationRegistry() {
        return serviceDispatchRegistry;
    }

    /**
     * Get the {@link DataAccessPointRegistry} for this action.
     */
    public DataAccessPointRegistry getDataAccessPointRegistry() {
        return dataAccessPointRegistry;
    }

    /** Set the endpoint and endpoint name that this is an action for.
     * @param endpoint {@link Endpoint}
     */
    public void setEndpoint(Endpoint endpoint) {
        if ( endpoint != null )
            this.context = Context.mergeCopy(getContext(), endpoint.getContext());
        this.endpoint = endpoint;
    }

    /** Get the endpoint for the action (may be null) . */
    public Endpoint getEndpoint() {
        return endpoint;
    }

    /**
     * Returns whether or not the underlying DatasetGraph is fully transactional (supports rollback)
     */
    public boolean isTransactional() {
        return isTransactional;
    }

    public void begin(TxnType txnType) {
        if ( transactional != null )
            transactional.begin(txnType);
        activeDSG = dsg;
        if ( dataService != null )
            dataService.startTxn(txnType);
    }

    public void begin() {
        begin(READ_PROMOTE);
    }

    public void beginWrite() {
        begin(WRITE);
    }

    public void beginRead() {
        begin(READ);
    }

    public void endRead() {
        if ( dataService != null )
            dataService.finishTxn();
        if ( transactional != null ) {
            try { transactional.commit(); } catch (RuntimeException ex) {}
            try { transactional.end(); } catch (RuntimeException ex) {}
        }
        activeDSG = null;
    }

    public void end() {
        dataService.finishTxn();

        if ( transactional.isInTransaction() ) {
            Log.warn(this, "Transaction still active - no commit or abort seen (forced abort)");
            try {
                transactional.abort();
            } catch (RuntimeException ex) {
                Log.warn(this, "Exception in forced abort (trying to continue)", ex);
            }
        }
        if ( transactional.isInTransaction() ) {
            try { transactional.end(); }
            catch (RuntimeException ex) {}
        }
        activeDSG = null;
    }

    public void commit() {
        dataService.finishTxn();
        transactional.commit();
        end();
    }

    /** Abort: ignore exceptions (for clearup code) */
    public void abortSilent() {
        try { transactional.abort(); }
        catch (Exception ex) {}
        finally {
            try { end(); } catch (Exception ex) {}
        }
    }

    public void abort() {
        try { transactional.abort(); }
        catch (Exception ex) {
            // Some datasets claim to be transactional but
            // don't provide a real abort. We tried to avoid
            // them earlier but even if they sneak through,
            // we try to continue server operation.
            Log.warn(this, "Exception during abort (operation attempts to continue): "+ex.getMessage());
        }
        end();
    }

    public final void startRequest() {
        if ( dataAccessPoint != null )
            dataAccessPoint.startRequest(this);
    }

    public final void finishRequest() {
        if ( dataAccessPoint != null )
            dataAccessPoint.finishRequest(this);
    }

    /** If inside the transaction for the action, return the active {@link DatasetGraph},
     *  otherwise return null.
     * @return Current active {@link DatasetGraph}
     */
    public final DatasetGraph getActiveDSG() {
        return activeDSG;
    }

    public final DataAccessPoint getDataAccessPoint() {
        return dataAccessPoint;
    }

//    public void setDataAccessPoint(DataAccessPoint dataAccessPoint) {
//        this.dataAccessPoint = dataAccessPoint;
//    }

    public final DataService getDataService() {
        return dataService;
    }

//    public final void setDataService(DataService dataService) {
//        this.dataService = dataService;
//    }

    public final String getDatasetName() {
        return datasetName;
    }

//    public void setDatasetName(String datasetName) {
//        this.datasetName = datasetName;
//    }

    /** Reduce to a size that can be kept around for sometime.
     */
    public void minimize() {
        this.request = null;
        this.response = null;
        this.dsg = null;
        this.dataService = null;
        this.activeDSG = null;
        this.endpoint = null;
    }

    public void setStartTime() {
        if ( startTimeIsSet )
            Log.warn(this,  "Start time reset");
        startTimeIsSet = true;
        this.startTime = System.nanoTime();
    }

    /** Start time, in system nanos */
    public long getStartTime() {
        if ( ! startTimeIsSet )
            Log.warn(this,  "Start time is not set");
        return startTime;
    }

    /** Start time, in system nanos */
    public long getFinishTime() {
        if ( ! finishTimeIsSet )
            Log.warn(this,  "Finish time is not set");
        return finishTime;
    }

    public void setFinishTime() {
        if ( finishTimeIsSet )
            Log.warn(this,  "Finish time reset");
        finishTimeIsSet = true;
        this.finishTime = System.nanoTime();
    }

    public String getMethod()                           { return request.getMethod(); }

    public HttpServletRequest getRequest()              { return request; }

    public HttpServletResponseTracker getResponse()     { return response; }

    /**
     * Return the recorded time taken in milliseconds. {@link #setStartTime} and
     * {@link #setFinishTime} must have been called.
     */
    public long getTime() {
        if ( !startTimeIsSet )
            Log.warn(this,  "Start time not set");
        if ( ! finishTimeIsSet )
            Log.warn(this,  "Finish time not set");
        return (finishTime-startTime)/(1000*1000);
    }

    public void sync() {
        SystemARQ.sync(dsg);
    }

    @Override
    public String toString() {
        return request.getMethod()+" "+request.getRequestURL().toString();
    }
}
