// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package postgresql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"

	"go.temporal.io/server/common/persistence/sql/sqlplugin"
)

const (
	templateCreateWorkflowExecutionStarted = `INSERT INTO executions_visibility (` +
		`namespace_id, workflow_id, run_id, start_time, execution_time, workflow_type_name, status, memo, encoding) ` +
		`VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
         ON CONFLICT (namespace_id, run_id) DO NOTHING`

	templateCreateWorkflowExecutionClosed = `INSERT INTO executions_visibility (` +
		`namespace_id, workflow_id, run_id, start_time, execution_time, workflow_type_name, close_time, status, history_length, memo, encoding) ` +
		`VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
		ON CONFLICT (namespace_id, run_id) DO UPDATE 
		  SET workflow_id = excluded.workflow_id,
		      start_time = excluded.start_time,
		      execution_time = excluded.execution_time,
              workflow_type_name = excluded.workflow_type_name,
			  close_time = excluded.close_time,
			  status = excluded.status,
			  history_length = excluded.history_length,
			  memo = excluded.memo,
			  encoding = excluded.encoding`

	// RunID condition is needed for correct pagination
	templateConditions1 = ` AND namespace_id = $1
		 AND start_time >= $2
		 AND start_time <= $3
		 AND ((run_id > $4 and start_time = $5) OR (start_time < $6))
         ORDER BY start_time DESC, run_id
         LIMIT $7`

	templateConditions2 = ` AND namespace_id = $2
		 AND start_time >= $3
		 AND start_time <= $4
		 AND ((run_id > $5 and start_time = $6) OR (start_time < $7))
         ORDER BY start_time DESC, run_id
		 LIMIT $8`

	templateConditionsClosedWorkflow1 = ` AND namespace_id = $1
		 AND close_time >= $2
		 AND close_time <= $3
		 AND ((run_id > $4 and close_time = $5) OR (close_time < $6))
         ORDER BY close_time DESC, run_id
         LIMIT $7`

	templateConditionsClosedWorkflow2 = ` AND namespace_id = $2
		 AND close_time >= $3
		 AND close_time <= $4
 		 AND ((run_id > $5 and close_time = $6) OR (close_time < $7))
         ORDER BY close_time DESC, run_id
         LIMIT $8`

	templateOpenFieldNames = `workflow_id, run_id, start_time, execution_time, workflow_type_name, status, memo, encoding`
	templateOpenSelect     = `SELECT ` + templateOpenFieldNames + ` FROM executions_visibility WHERE status = 1 `

	templateClosedSelect = `SELECT ` + templateOpenFieldNames + `, close_time, history_length
		 FROM executions_visibility WHERE status != 1 `

	templateGetOpenWorkflowExecutions = templateOpenSelect + templateConditions1

	templateGetClosedWorkflowExecutions = templateClosedSelect + templateConditionsClosedWorkflow1

	templateGetOpenWorkflowExecutionsByType = templateOpenSelect + `AND workflow_type_name = $1` + templateConditions2

	templateGetClosedWorkflowExecutionsByType = templateClosedSelect + `AND workflow_type_name = $1` + templateConditionsClosedWorkflow2

	templateGetOpenWorkflowExecutionsByID = templateOpenSelect + `AND workflow_id = $1` + templateConditions2

	templateGetClosedWorkflowExecutionsByID = templateClosedSelect + `AND workflow_id = $1` + templateConditionsClosedWorkflow2

	templateGetClosedWorkflowExecutionsByStatus = templateClosedSelect + `AND status = $1` + templateConditionsClosedWorkflow2

	templateGetClosedWorkflowExecution = `SELECT workflow_id, run_id, start_time, execution_time, memo, encoding, close_time, workflow_type_name, status, history_length 
		 FROM executions_visibility
		 WHERE namespace_id = $1 AND status != 1
		 AND run_id = $2`

	templateDeleteWorkflowExecution = "DELETE FROM executions_visibility WHERE namespace_id = $1 AND run_id = $2"
)

var errCloseParams = errors.New("missing one of {closeTime, historyLength} params")

// InsertIntoVisibility inserts a row into visibility table. If an row already exist,
// its left as such and no update will be made
func (pdb *db) InsertIntoVisibility(
	ctx context.Context,
	row *sqlplugin.VisibilityRow,
) (sql.Result, error) {
	row.StartTime = pdb.converter.ToPostgreSQLDateTime(row.StartTime)
	return pdb.conn.ExecContext(ctx,
		templateCreateWorkflowExecutionStarted,
		row.NamespaceID,
		row.WorkflowID,
		row.RunID,
		row.StartTime,
		row.ExecutionTime,
		row.WorkflowTypeName,
		row.Status,
		row.Memo,
		row.Encoding,
	)
}

// ReplaceIntoVisibility replaces an existing row if it exist or creates a new row in visibility table
func (pdb *db) ReplaceIntoVisibility(
	ctx context.Context,
	row *sqlplugin.VisibilityRow,
) (sql.Result, error) {
	switch {
	case row.CloseTime != nil && row.HistoryLength != nil:
		row.StartTime = pdb.converter.ToPostgreSQLDateTime(row.StartTime)
		closeTime := pdb.converter.ToPostgreSQLDateTime(*row.CloseTime)
		return pdb.conn.ExecContext(ctx,
			templateCreateWorkflowExecutionClosed,
			row.NamespaceID,
			row.WorkflowID,
			row.RunID,
			row.StartTime,
			row.ExecutionTime,
			row.WorkflowTypeName,
			closeTime,
			row.Status,
			*row.HistoryLength,
			row.Memo,
			row.Encoding,
		)
	default:
		return nil, errCloseParams
	}
}

// DeleteFromVisibility deletes a row from visibility table if it exist
func (pdb *db) DeleteFromVisibility(
	ctx context.Context,
	filter sqlplugin.VisibilityDeleteFilter,
) (sql.Result, error) {
	return pdb.conn.ExecContext(ctx,
		templateDeleteWorkflowExecution,
		filter.NamespaceID,
		filter.RunID,
	)
}

// SelectFromVisibility reads one or more rows from visibility table
func (pdb *db) SelectFromVisibility(
	ctx context.Context,
	filter sqlplugin.VisibilitySelectFilter,
) ([]sqlplugin.VisibilityRow, error) {
	var err error
	var rows []sqlplugin.VisibilityRow
	if filter.MinTime != nil {
		*filter.MinTime = pdb.converter.ToPostgreSQLDateTime(*filter.MinTime)
	}
	if filter.MaxTime != nil {
		*filter.MaxTime = pdb.converter.ToPostgreSQLDateTime(*filter.MaxTime)
	}
	// If filter.Status == 0 (UNSPECIFIED) then only closed workflows will be returned (all excluding 1 (RUNNING)).
	switch {
	case filter.MinTime == nil && filter.RunID != nil && filter.Status != 1:
		var row sqlplugin.VisibilityRow
		err = pdb.conn.GetContext(ctx,
			&row,
			templateGetClosedWorkflowExecution,
			filter.NamespaceID,
			*filter.RunID,
		)
		if err == nil {
			rows = append(rows, row)
		}
	case filter.MinTime != nil && filter.MaxTime != nil &&
		filter.WorkflowID != nil && filter.RunID != nil && filter.PageSize != nil:
		qry := templateGetOpenWorkflowExecutionsByID
		if filter.Status != 1 {
			qry = templateGetClosedWorkflowExecutionsByID
		}
		err = pdb.conn.SelectContext(ctx,
			&rows,
			qry,
			*filter.WorkflowID,
			filter.NamespaceID,
			*filter.MinTime,
			*filter.MaxTime,
			*filter.RunID,
			*filter.MaxTime,
			*filter.MaxTime,
			*filter.PageSize)
	case filter.MinTime != nil && filter.MaxTime != nil &&
		filter.WorkflowTypeName != nil && filter.RunID != nil && filter.PageSize != nil:
		qry := templateGetOpenWorkflowExecutionsByType
		if filter.Status != 1 {
			qry = templateGetClosedWorkflowExecutionsByType
		}
		err = pdb.conn.SelectContext(ctx,
			&rows,
			qry,
			*filter.WorkflowTypeName,
			filter.NamespaceID,
			*filter.MinTime,
			*filter.MaxTime,
			*filter.RunID,
			*filter.MaxTime,
			*filter.MaxTime,
			*filter.PageSize)
	case filter.MinTime != nil && filter.MaxTime != nil &&
		filter.RunID != nil && filter.PageSize != nil &&
		filter.Status != 0 && filter.Status != 1: // 0 is UNSPECIFIED, 1 is RUNNING
		err = pdb.conn.SelectContext(ctx,
			&rows,
			templateGetClosedWorkflowExecutionsByStatus,
			filter.Status,
			filter.NamespaceID,
			*filter.MinTime,
			*filter.MaxTime,
			*filter.RunID,
			*filter.MaxTime,
			*filter.MaxTime,
			*filter.PageSize)
	case filter.MinTime != nil && filter.MaxTime != nil &&
		filter.RunID != nil && filter.PageSize != nil:
		qry := templateGetOpenWorkflowExecutions
		if filter.Status != 1 {
			qry = templateGetClosedWorkflowExecutions
		}
		err = pdb.conn.SelectContext(ctx,
			&rows,
			qry,
			filter.NamespaceID,
			*filter.MinTime,
			*filter.MaxTime,
			*filter.RunID,
			*filter.MaxTime,
			*filter.MaxTime,
			*filter.PageSize)
	default:
		return nil, fmt.Errorf("invalid query filter")
	}
	if err != nil {
		return nil, err
	}
	for i := range rows {
		rows[i].StartTime = pdb.converter.FromPostgreSQLDateTime(rows[i].StartTime)
		rows[i].ExecutionTime = pdb.converter.FromPostgreSQLDateTime(rows[i].ExecutionTime)
		if rows[i].CloseTime != nil {
			closeTime := pdb.converter.FromPostgreSQLDateTime(*rows[i].CloseTime)
			rows[i].CloseTime = &closeTime
		}
		// need to trim the run ID, or otherwise the returned value will
		//  come with lots of trailing spaces, probably due to the CHAR(64) type
		rows[i].RunID = strings.TrimSpace(rows[i].RunID)
	}
	return rows, nil
}
