package db

import (
	"database/sql"
	"errors"
	"github.com/openaccounting/oa-server/core/model/types"
	"github.com/openaccounting/oa-server/core/util"
	"time"
)

type OrgInterface interface {
	CreateOrg(*types.Org, string, []*types.Account) error
	UpdateOrg(*types.Org) error
	GetOrg(string, string) (*types.Org, error)
	GetOrgs(string) ([]*types.Org, error)
	GetOrgUserIds(string) ([]string, error)
	InsertInvite(*types.Invite) error
	AcceptInvite(*types.Invite, string) error
	GetInvites(string) ([]*types.Invite, error)
	GetInvite(string) (*types.Invite, error)
	DeleteInvite(string) error
}

const orgFields = "LOWER(HEX(o.id)),o.inserted,o.updated,o.name,o.currency,o.`precision`,o.timezone"
const inviteFields = "i.id,LOWER(HEX(i.orgId)),i.inserted,i.updated,i.email,i.accepted"

func (db *DB) CreateOrg(org *types.Org, userId string, accounts []*types.Account) (err error) {
	tx, err := db.Begin()

	if err != nil {
		return
	}

	defer func() {
		if p := recover(); p != nil {
			tx.Rollback()
			panic(p) // re-throw panic after Rollback
		} else if err != nil {
			tx.Rollback()
		} else {
			err = tx.Commit()
		}
	}()

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

	// create org
	query1 := "INSERT INTO org(id,inserted,updated,name,currency,`precision`,timezone) VALUES(UNHEX(?),?,?,?,?,?,?)"

	res, err := tx.Exec(
		query1,
		org.Id,
		util.TimeToMs(org.Inserted),
		util.TimeToMs(org.Updated),
		org.Name,
		org.Currency,
		org.Precision,
		org.Timezone,
	)

	if err != nil {
		return
	}

	// associate user with org
	query2 := "INSERT INTO userorg(userId,orgId,admin) VALUES(UNHEX(?),UNHEX(?), 1)"

	res, err = tx.Exec(query2, userId, org.Id)

	if err != nil {
		return
	}

	_, err = res.LastInsertId()

	if err != nil {
		return
	}

	// create Accounts: Root, Assets, Liabilities, Equity, Income, Expenses

	for _, account := range accounts {

		query := "INSERT INTO account(id,orgId,inserted,updated,name,parent,currency,`precision`,debitBalance) VALUES (UNHEX(?),UNHEX(?),?,?,?,UNHEX(?),?,?,?)"

		if _, err = tx.Exec(
			query,
			account.Id,
			org.Id,
			util.TimeToMs(org.Inserted),
			util.TimeToMs(org.Updated),
			account.Name,
			account.Parent,
			account.Currency,
			account.Precision,
			account.DebitBalance,
		); err != nil {
			return
		}
	}

	permissionId, err := util.NewGuid()

	if err != nil {
		return
	}

	// Grant root permission to user

	query3 := "INSERT INTO permission (id,userId,orgId,accountId,type,inserted,updated) VALUES(UNHEX(?),UNHEX(?),UNHEX(?),UNHEX(?),?,?,?)"

	_, err = tx.Exec(
		query3,
		permissionId,
		userId,
		org.Id,
		accounts[0].Id,
		0,
		util.TimeToMs(org.Inserted),
		util.TimeToMs(org.Updated),
	)

	return
}

func (db *DB) UpdateOrg(org *types.Org) error {
	org.Updated = time.Now()

	query := "UPDATE org SET updated = ?, name = ?, timezone = ? WHERE id = UNHEX(?)"
	_, err := db.Exec(
		query,
		util.TimeToMs(org.Updated),
		org.Name,
		org.Timezone,
		org.Id,
	)

	return err
}

func (db *DB) GetOrg(orgId string, userId string) (*types.Org, error) {
	var o types.Org
	var inserted int64
	var updated int64

	err := db.QueryRow("SELECT "+orgFields+" FROM org o JOIN userorg ON userorg.orgId = o.id WHERE o.id = UNHEX(?) AND userorg.userId = UNHEX(?)", orgId, userId).
		Scan(&o.Id, &inserted, &updated, &o.Name, &o.Currency, &o.Precision, &o.Timezone)

	switch {
	case err == sql.ErrNoRows:
		return nil, errors.New("Org not found")
	case err != nil:
		return nil, err
	default:
		o.Inserted = util.MsToTime(inserted)
		o.Updated = util.MsToTime(updated)
		return &o, nil
	}
}

func (db *DB) GetOrgs(userId string) ([]*types.Org, error) {
	rows, err := db.Query("SELECT "+orgFields+" from org o JOIN userorg ON userorg.orgId = o.id WHERE userorg.userId = UNHEX(?)", userId)

	if err != nil {
		return nil, err
	}

	defer rows.Close()

	orgs := make([]*types.Org, 0)

	for rows.Next() {
		o := new(types.Org)
		var inserted int64
		var updated int64

		err = rows.Scan(&o.Id, &inserted, &updated, &o.Name, &o.Currency, &o.Precision, &o.Timezone)
		if err != nil {
			return nil, err
		}

		o.Inserted = util.MsToTime(inserted)
		o.Updated = util.MsToTime(updated)

		orgs = append(orgs, o)
	}
	err = rows.Err()
	if err != nil {
		return nil, err
	}

	return orgs, nil
}

func (db *DB) GetOrgUserIds(orgId string) ([]string, error) {
	rows, err := db.Query("SELECT LOWER(HEX(userId)) FROM userorg WHERE orgId = UNHEX(?)", orgId)

	if err != nil {
		return nil, err
	}

	defer rows.Close()

	userIds := make([]string, 0)

	for rows.Next() {
		var userId string
		err = rows.Scan(&userId)
		if err != nil {
			return nil, err
		}

		userIds = append(userIds, userId)
	}

	err = rows.Err()
	if err != nil {
		return nil, err
	}

	return userIds, nil
}

func (db *DB) InsertInvite(invite *types.Invite) error {
	invite.Inserted = time.Now()
	invite.Updated = invite.Inserted

	query := "INSERT INTO invite(id,orgId,inserted,updated,email,accepted) VALUES(?,UNHEX(?),?,?,?,?)"
	_, err := db.Exec(
		query,
		invite.Id,
		invite.OrgId,
		util.TimeToMs(invite.Inserted),
		util.TimeToMs(invite.Updated),
		invite.Email,
		false,
	)

	return err
}

func (db *DB) AcceptInvite(invite *types.Invite, userId string) error {
	invite.Updated = time.Now()

	// Get root account for permission
	rootAccount, err := db.GetRootAccount(invite.OrgId)

	if err != nil {
		return err
	}

	tx, err := db.Begin()

	if err != nil {
		return err
	}

	defer func() {
		if p := recover(); p != nil {
			tx.Rollback()
			panic(p) // re-throw panic after Rollback
		} else if err != nil {
			tx.Rollback()
		} else {
			err = tx.Commit()
		}
	}()

	// associate user with org
	query1 := "INSERT INTO userorg(userId,orgId,admin) VALUES(UNHEX(?),UNHEX(?), 0)"

	_, err = tx.Exec(query1, userId, invite.OrgId)

	if err != nil {
		return err
	}

	query2 := "UPDATE invite SET accepted = 1, updated = ? WHERE id = ?"

	_, err = tx.Exec(query2, util.TimeToMs(invite.Updated), invite.Id)

	// Grant root permission to user

	permissionId, err := util.NewGuid()

	if err != nil {
		return err
	}

	query3 := "INSERT INTO permission (id,userId,orgId,accountId,type,inserted,updated) VALUES(UNHEX(?),UNHEX(?),UNHEX(?),UNHEX(?),?,?,?)"

	_, err = tx.Exec(
		query3,
		permissionId,
		userId,
		invite.OrgId,
		rootAccount.Id,
		0,
		util.TimeToMs(invite.Updated),
		util.TimeToMs(invite.Updated),
	)

	return err
}

func (db *DB) GetInvites(orgId string) ([]*types.Invite, error) {
	// don't include expired invoices
	cutoff := util.TimeToMs(time.Now()) - 7*24*60*60*1000

	rows, err := db.Query("SELECT "+inviteFields+" FROM invite i WHERE orgId = UNHEX(?) AND inserted > ?", orgId, cutoff)

	if err != nil {
		return nil, err
	}

	defer rows.Close()

	invites := make([]*types.Invite, 0)

	for rows.Next() {
		i := new(types.Invite)
		var inserted int64
		var updated int64

		err = rows.Scan(&i.Id, &i.OrgId, &inserted, &updated, &i.Email, &i.Accepted)
		if err != nil {
			return nil, err
		}

		i.Inserted = util.MsToTime(inserted)
		i.Updated = util.MsToTime(updated)

		invites = append(invites, i)
	}
	err = rows.Err()
	if err != nil {
		return nil, err
	}

	return invites, nil
}

func (db *DB) GetInvite(id string) (*types.Invite, error) {
	var i types.Invite
	var inserted int64
	var updated int64

	err := db.QueryRow("SELECT "+inviteFields+" FROM invite i WHERE i.id = ?", id).
		Scan(&i.Id, &i.OrgId, &inserted, &updated, &i.Email, &i.Accepted)

	switch {
	case err == sql.ErrNoRows:
		return nil, errors.New("Invite not found")
	case err != nil:
		return nil, err
	default:
		i.Inserted = util.MsToTime(inserted)
		i.Updated = util.MsToTime(updated)
		return &i, nil
	}
}

func (db *DB) DeleteInvite(id string) error {
	query := "DELETE FROM invite WHERE id = ?"
	_, err := db.Exec(
		query,
		id,
	)

	return err
}
