package model

import (
	"errors"
	"fmt"
	"time"

	"github.com/openaccounting/oa-server/core/model/types"
	"github.com/openaccounting/oa-server/core/ws"
)

type TransactionInterface interface {
	CreateTransaction(*types.Transaction) error
	UpdateTransaction(string, *types.Transaction) error
	GetTransactionsByAccount(string, string, string, *types.QueryOptions) ([]*types.Transaction, error)
	GetTransactionsByOrg(string, string, *types.QueryOptions) ([]*types.Transaction, error)
	DeleteTransaction(string, string, string) error
}

func (model *Model) CreateTransaction(transaction *types.Transaction) (err error) {
	err = model.checkSplits(transaction)

	if err != nil {
		return
	}

	if transaction.Id == "" {
		return errors.New("id required")
	}

	transaction.Inserted = time.Now()
	transaction.Updated = time.Now()

	if transaction.Date.IsZero() {
		transaction.Date = transaction.Inserted
	}

	err = model.db.InsertTransaction(transaction)

	if err != nil {
		return
	}

	// Notify web socket subscribers
	// TODO only get user ids that have permission to access transaction
	userIds, err2 := model.db.GetOrgUserIds(transaction.OrgId)

	if err2 == nil {
		ws.PushTransaction(transaction, userIds, "create")
	}

	return
}

func (model *Model) UpdateTransaction(oldId string, transaction *types.Transaction) (err error) {
	err = model.checkSplits(transaction)

	if err != nil {
		return
	}

	if oldId == "" || transaction.Id == "" {
		return errors.New("id required")
	}

	// Get original transaction
	original, err := model.getTransactionById(oldId)

	if err != nil {
		return
	}

	transaction.Updated = time.Now()
	transaction.Inserted = original.Inserted

	// We used to compare splits and if they hadn't changed just do an update
	// on the transaction. The problem is then the updated field gets out of sync
	// between the tranaction and its splits.
	// It needs to be in sync for getTransactionsByOrg() to work correctly with pagination

	// Delete old transaction and insert a new one
	transaction.Inserted = transaction.Updated
	err = model.db.DeleteAndInsertTransaction(oldId, transaction)

	if err != nil {
		return
	}

	// Notify web socket subscribers
	// TODO only get user ids that have permission to access transaction
	userIds, err2 := model.db.GetOrgUserIds(transaction.OrgId)

	if err2 == nil {
		ws.PushTransaction(original, userIds, "delete")
		ws.PushTransaction(transaction, userIds, "create")
	}

	return
}

func (model *Model) GetTransactionsByAccount(orgId string, userId string, accountId string, options *types.QueryOptions) ([]*types.Transaction, error) {
	userAccounts, err := model.GetAccounts(orgId, userId, "")

	if err != nil {
		return nil, err
	}

	if !model.accountsContainWriteAccess(userAccounts, accountId) {
		return nil, fmt.Errorf("%s %s", "user does not have permission to access account", accountId)
	}

	return model.db.GetTransactionsByAccount(accountId, options)

}

func (model *Model) GetTransactionsByOrg(orgId string, userId string, options *types.QueryOptions) ([]*types.Transaction, error) {
	userAccounts, err := model.GetAccounts(orgId, userId, "")

	if err != nil {
		return nil, err
	}

	var accountIds []string
	for _, account := range userAccounts {
		accountIds = append(accountIds, account.Id)
	}

	return model.db.GetTransactionsByOrg(orgId, options, accountIds)
}

func (model *Model) DeleteTransaction(id string, userId string, orgId string) (err error) {
	transaction, err := model.getTransactionById(id)

	if err != nil {
		return
	}

	userAccounts, err := model.GetAccounts(orgId, userId, "")

	if err != nil {
		return
	}

	for _, split := range transaction.Splits {
		if !model.accountsContainWriteAccess(userAccounts, split.AccountId) {
			return fmt.Errorf("%s %s", "user does not have permission to access account", split.AccountId)
		}
	}

	err = model.db.DeleteTransaction(id)

	if err != nil {
		return
	}

	// Notify web socket subscribers
	// TODO only get user ids that have permission to access transaction
	userIds, err2 := model.db.GetOrgUserIds(transaction.OrgId)

	if err2 == nil {
		ws.PushTransaction(transaction, userIds, "delete")
	}

	return
}

func (model *Model) getTransactionById(id string) (*types.Transaction, error) {
	// TODO if this is made public, make a separate version that checks permission
	return model.db.GetTransactionById(id)
}

func (model *Model) GetTransaction(transactionId, orgId, userId string) (*types.Transaction, error) {
	transaction, err := model.getTransactionById(transactionId)
	if err != nil {
		return nil, err
	}

	if transaction == nil || transaction.OrgId != orgId {
		return nil, nil
	}

	// Check if user has access to all accounts in the transaction
	userAccounts, err := model.GetAccounts(orgId, userId, "")
	if err != nil {
		return nil, err
	}

	for _, split := range transaction.Splits {
		if !model.accountsContainReadAccess(userAccounts, split.AccountId) {
			return nil, fmt.Errorf("user does not have permission to access account %s", split.AccountId)
		}
	}

	return transaction, nil
}

func (model *Model) checkSplits(transaction *types.Transaction) (err error) {
	if len(transaction.Splits) < 2 {
		return errors.New("at least 2 splits are required")
	}

	org, err := model.GetOrg(transaction.OrgId, transaction.UserId)

	if err != nil {
		return
	}

	userAccounts, err := model.GetAccounts(transaction.OrgId, transaction.UserId, "")

	if err != nil {
		return
	}

	var amount int64 = 0

	for _, split := range transaction.Splits {
		if !model.accountsContainWriteAccess(userAccounts, split.AccountId) {
			return fmt.Errorf("%s %s", "user does not have permission to access account", split.AccountId)
		}

		account := model.getAccountFromList(userAccounts, split.AccountId)

		if !account.HasChildren {
			return errors.New("cannot use parent account for split")
		}

		if account.Currency == org.Currency && split.NativeAmount != split.Amount {
			return errors.New("nativeAmount must equal amount for native currency splits")
		}

		amount += split.NativeAmount
	}

	if amount != 0 {
		return errors.New("splits must add up to 0")
	}

	return
}
