package datamysql

import (
	"database/sql"
	"fmt"

	"bitbucket.org/nemt/nemt-portal-api/domain/entity"
	"bitbucket.org/nemt/nemt-portal-api/infra/errors"
	uuid "github.com/satori/go.uuid"
)

// rideRepo maps methods to database
type organizationRepo struct {
	conn executor
}

func newOrganizationRepo(conn executor) *organizationRepo {
	return &organizationRepo{
		conn: conn,
	}
}

func (c *organizationRepo) getProfileQuery(user entity.User) (query string, where string, err error) {
	if len(user.Profiles) > 0 {
		for _, p := range user.Profiles {
			switch p.Key {
			case "VIRPT":
				switch p.Organization.Type.Key {
				case "provider":
					return
				}
			case "AD", "BCBSIAD", "BDCAD", "PLANAD":
				switch p.Organization.Type.Key {
				case "techsupport", "bcbsi", "bcbsa":
					return
				case "plan":
					query = ` INNER JOIN       tab_provider                            e
                                ON                  e.provider_id                   =       a.organization_reference_id
                               AND                  b.organization_type_key         =       'plan'
                         LEFT JOIN       tab_organization                        f
                                ON                  f.organization_id               =       a.organization_parent_id `
					where = fmt.Sprintf(`	AND (a.organization_uuid = '%s' OR f.organization_uuid = '%s') `, p.Organization.UUID, p.Organization.UUID)
				case "provider":
					query = ` INNER JOIN       tab_provider                            e
                                ON                  e.provider_id                   =       a.organization_reference_id
                               AND                  b.organization_type_key         =       'provider'
                         LEFT JOIN       tab_organization                        f
                                ON                  f.organization_id               =       a.organization_parent_id `
					where = fmt.Sprintf(`	AND (a.organization_uuid = '%s' OR f.organization_uuid = '%s') `, p.Organization.UUID, p.Organization.UUID)
					return
				}
			case "SP", "SPT":
				switch p.Organization.Type.Key {
				case "techsupport", "bcbsi", "bcbsa":
					return
				case "plan":
					query = ` INNER JOIN       tab_provider                            e
                                ON                  e.provider_id                   =       a.organization_reference_id
                               AND                  b.organization_type_key         =       'plan'
                         LEFT JOIN       tab_organization                        f
                                ON                  f.organization_id               =       a.organization_parent_id `
					where = fmt.Sprintf(`	AND (a.organization_uuid = '%s' OR f.organization_uuid = '%s') `, p.Organization.UUID, p.Organization.UUID)
				case "provider":
					query = ` INNER JOIN       tab_provider                            e
                                ON                  e.provider_id                   =       a.organization_reference_id
                               AND                  b.organization_type_key         =       'provider'
                         LEFT JOIN       tab_organization                        f
                                ON                  f.organization_id               =       a.organization_parent_id `
					where = fmt.Sprintf(`	AND (a.organization_uuid = '%s' OR f.organization_uuid = '%s') `, p.Organization.UUID, p.Organization.UUID)
					return
				}
			}
		}

		if query == "" && where == "" {
			return "", "", fmt.Errorf("Invalid Query")
		} else {
			return
		}
	} else {
		return "", "", fmt.Errorf("User has no profile to search")
	}
}

func (c *organizationRepo) getQuery() string {
	const (
		query = `SELECT 
										a.organization_id,
										a.organization_uuid,
										b.organization_type_id,
										b.organization_type,
										b.organization_type_key,
										a.organization_name,
										a.organization_description,
										a.organization_reference_id,
										a.organization_parent_id,
										(a.main_organization = b'1') main_organization,
										a.create_at,
										a.update_at,
										(a.active = b'1') active,
										(a.suspended = b'1') suspended,
										(a.blocked = b'1') blocked,
										c.user_id created_user_id,
										c.user_uuid created_user_uuid,
										c.name created_user_name,
										d.user_id updated_user_id,
										d.user_uuid updated_user_uuid,
										d.name updated_user_name
							FROM
														tab_organization        a
						INNER JOIN     tab_organization_type   b
								ON          a.organization_type_id  =   b.organization_type_id 
						INNER JOIN		 tab_user								 c
								ON					c.user_id 						=		a.created_user
						INNER JOIN		 tab_user								 d
								ON					d.user_id 						=		a.updated_user `
	)

	return query
}
func (c *organizationRepo) getAddressQuery() string {
	const query = `SELECT 
														a.organization_address_id, 
														a.organization_address_uuid, 
														a.internal_id,
														a.name,
														a.address,
														a.desc,
														a.lat,
														a.long,
														a.created,
														a.created_user_id,
														c.user_uuid created_user_uuid,
														c.name created_user_name,
														a.updated,
														a.updated_user_id,
														d.user_uuid updated_user_uuid,
														d.name updated_user_name,
														(a.active = b'1') active
									FROM      tab_organization_address         a 
						INNER JOIN      tab_organization    b
										ON      b.organization_id = a.organization_id
						INNER JOIN      tab_user            c
										ON      c.user_id         = a.created_user_id
						INNER JOIN      tab_user            d
										ON      d.user_id         = a.updated_user_id `

	return query
}

func (c *organizationRepo) getContactQuery() string {
	const (
		query = `SELECT 
														a.organization_contact_id, 
														a.organization_contact_uuid, 
														b.contact_type_id, 
														b.key               contact_type_key, 
														b.name              contact_type_name,
														a.contact,
														a.contact_name,
														a.contact_desc,
														a.created,
														a.created_user_id,
														d.user_uuid created_user_uuid,
														d.name created_user_name,
														a.updated,
														a.updated_user_id,
														e.user_uuid updated_user_uuid,
														e.name updated_user_name,
														(a.active = b'1') active
									FROM      tab_organization_contact         a 
						INNER JOIN      tab_contact_type    b 
										ON      a.contact_type_id = b.contact_type_id 
						INNER JOIN      tab_organization    c
										ON      c.organization_id = a.organization_id
						INNER JOIN      tab_user            d
										ON      d.user_id         = a.created_user_id
						INNER JOIN      tab_user            e
										ON      e.user_id         = a.updated_user_id `
	)

	return query
}

func (c *organizationRepo) getTypeQuery() string {
	const (
		query = `SELECT
										organization_type_id,
										organization_type,
										organization_type_key,
										description,
										create_at,
										update_at
							FROM
										tab_organization_type `
	)

	return query
}

// parseSet parses a result set result to an entity array
func (c *organizationRepo) parseSet(rows *sql.Rows, err error) ([]entity.Organization, error) {
	if err != nil {
		return nil, errors.Wrap(err)
	}
	result := make([]entity.Organization, 0)
	for rows.Next() {
		entity, err := c.parseEntity(rows)
		if err != nil {
			return nil, errors.Wrap(err)
		}
		result = append(result, entity)
	}
	return result, nil
}

// parseEntity parses a result to an entity
func (c *organizationRepo) parseEntity(row scanner) (retVal entity.Organization, err error) {
	err = row.Scan(
		&retVal.ID, &retVal.UUID, &retVal.Type.ID, &retVal.Type.Name, &retVal.Type.Key, &retVal.Name, &retVal.Description, &retVal.ReferenceID, &retVal.ParentID, &retVal.Main, &retVal.Created, &retVal.Updated, &retVal.Active, &retVal.Suspended, &retVal.Blocked, &retVal.Author.ID, &retVal.Author.UUID, &retVal.Author.Name, &retVal.LastEditor.ID, &retVal.LastEditor.UUID, &retVal.LastEditor.Name)

	if err == sql.ErrNoRows {
		return retVal, nil
	} else {
		return retVal, errors.Wrap(err)
	}
}

// parseSet parses a result set result to an entity array
func (c *organizationRepo) parseContactSet(rows *sql.Rows, err error) ([]entity.OrganizationContact, error) {
	if err != nil {
		return nil, errors.Wrap(err)
	}
	result := make([]entity.OrganizationContact, 0)
	for rows.Next() {
		entity, err := c.parseContactEntity(rows)
		if err != nil {
			return nil, errors.Wrap(err)
		}
		result = append(result, entity)
	}
	return result, nil
}

// parseEntity parses a result to an entity
func (c *organizationRepo) parseContactEntity(row scanner) (retVal entity.OrganizationContact, err error) {
	err = row.Scan(
		&retVal.ID, &retVal.UUID, &retVal.Type.ID, &retVal.Type.Key, &retVal.Type.Value, &retVal.Contact, &retVal.Name, &retVal.Description, &retVal.Created, &retVal.CreatedUser.ID, &retVal.CreatedUser.UUID, &retVal.CreatedUser.Name, &retVal.Updated, &retVal.UpdatedUser.ID, &retVal.UpdatedUser.UUID, &retVal.UpdatedUser.Name, &retVal.Active)

	return retVal, errors.Wrap(err)
}

// parseSet parses a result set result to an entity array
func (c *organizationRepo) parseAddressSet(rows *sql.Rows, err error) ([]entity.OrganizationAddress, error) {
	if err != nil {
		return nil, errors.Wrap(err)
	}
	result := make([]entity.OrganizationAddress, 0)
	for rows.Next() {
		entity, err := c.parseAddressEntity(rows)
		if err != nil {
			return nil, errors.Wrap(err)
		}
		result = append(result, entity)
	}
	return result, nil
}

// parseEntity parses a result to an entity
func (c *organizationRepo) parseAddressEntity(row scanner) (retVal entity.OrganizationAddress, err error) {
	err = row.Scan(
		&retVal.ID, &retVal.UUID, &retVal.InternalID, &retVal.Name, &retVal.Address, &retVal.Description, &retVal.Latitude, &retVal.Longitude, &retVal.Created, &retVal.CreatedUser.ID, &retVal.CreatedUser.UUID, &retVal.CreatedUser.Name, &retVal.Updated, &retVal.UpdatedUser.ID, &retVal.UpdatedUser.UUID, &retVal.UpdatedUser.Name, &retVal.Active)

	return retVal, errors.Wrap(err)
}

// parseSet parses a result set result to an entity array
func (c *organizationRepo) parseTypeSet(rows *sql.Rows, err error) ([]entity.OrganizationType, error) {
	if err != nil {
		return nil, errors.Wrap(err)
	}
	result := make([]entity.OrganizationType, 0)
	for rows.Next() {
		entity, err := c.parseTypeEntity(rows)
		if err != nil {
			return nil, errors.Wrap(err)
		}
		result = append(result, entity)
	}
	return result, nil
}

// parseEntity parses a result to an entity
func (c *organizationRepo) parseTypeEntity(row scanner) (retVal entity.OrganizationType, err error) {
	err = row.Scan(
		&retVal.ID, &retVal.Name, &retVal.Key, &retVal.Description, &retVal.Created, &retVal.Updated)

	return retVal, errors.Wrap(err)
}

func (c *organizationRepo) GetAllTypes() ([]entity.OrganizationType, error) {
	return c.parseTypeSet(c.conn.Query(c.getTypeQuery()))
}

func (c *organizationRepo) GetTypeByKey(key string) (entity.OrganizationType, error) {
	return c.parseTypeEntity(c.conn.QueryRow(c.getTypeQuery()+" WHERE organization_type_key=? ", key))
}

func (c *organizationRepo) GetByType(organizationTypeKey string, user entity.User) ([]entity.Organization, error) {
	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return nil, err
	}

	if organizationTypeKey == "" {
		return c.parseSet(c.conn.Query(c.getQuery() + query + " WHERE 1 = 1 " + where))
	} else {
		return c.parseSet(c.conn.Query(c.getQuery()+query+" WHERE b.organization_type_key = ? "+where, organizationTypeKey))
	}
}

func (c *organizationRepo) GetByName(name string, searchType string, user entity.User) ([]entity.Organization, error) {
	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return nil, err
	}

	finalQuery := c.getQuery() + query + " WHERE a.organization_name LIKE ? "
	switch searchType {
	case "parent":
		finalQuery += " AND a.organization_parent_id > 0 "
	case "child":
		finalQuery += " AND a.organization_parent_id = 0 "
	}

	finalQuery += where
	name = "%" + name + "%"

	return c.parseSet(c.conn.Query(finalQuery, name))
}

func (c *organizationRepo) GetByUUID(organizationUUID string, user entity.User) (entity.Organization, error) {
	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return entity.Organization{}, err
	}

	query = c.getQuery() + query + " WHERE a.organization_uuid = ? " + where
	return c.parseEntity(c.conn.QueryRow(query, organizationUUID))
}

func (c *organizationRepo) GetByID(organizationID int64, user entity.User) (entity.Organization, error) {
	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return entity.Organization{}, err
	}

	query = c.getQuery() + query + " WHERE a.organization_id = ? " + where

	return c.parseEntity(c.conn.QueryRow(query, organizationID))
}

func (c *organizationRepo) GetByTypeAndReferenceID(typeKey string, referenceID int64, user entity.User) (entity.Organization, error) {
	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return entity.Organization{}, err
	}

	query = c.getQuery() + query + " WHERE b.organization_type_key = ? AND  a.organization_reference_id = ? " + where

	return c.parseEntity(c.conn.QueryRow(query, typeKey, referenceID))
}

func (c *organizationRepo) GetChildsByID(organizationID int64, user entity.User) ([]entity.Organization, error) {
	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return nil, err
	}

	query = c.getQuery() + query + " WHERE a.organization_parent_id = ? " + where

	return c.parseSet(c.conn.Query(query, organizationID))
}

func (c *organizationRepo) GetContactsByOrganizationUUID(organizationUUID string) ([]entity.OrganizationContact, error) {
	return c.parseContactSet(c.conn.Query(c.getContactQuery()+" WHERE a.active = 1 AND c.organization_uuid = ? ", organizationUUID))
}

func (c *organizationRepo) GetContactsByOrganizationID(organizationID int64) ([]entity.OrganizationContact, error) {
	return c.parseContactSet(c.conn.Query(c.getContactQuery()+" WHERE a.active = 1 AND c.organization_id = ? ", organizationID))
}

func (c *organizationRepo) GetContactsByUUID(contactUUID string) (entity.OrganizationContact, error) {
	return c.parseContactEntity(c.conn.QueryRow(c.getContactQuery()+" WHERE a.organization_contact_uuid = ? ", contactUUID))
}

func (c *organizationRepo) GetAddressByOrganizationUUID(organizationUUID string) ([]entity.OrganizationAddress, error) {
	return c.parseAddressSet(c.conn.Query(c.getAddressQuery()+" WHERE a.active = 1 AND b.organization_uuid = ? ", organizationUUID))
}

func (c *organizationRepo) GetAddressByOrganizationID(organizationID int64) ([]entity.OrganizationAddress, error) {
	return c.parseAddressSet(c.conn.Query(c.getAddressQuery()+" WHERE a.active = 1 AND b.organization_id = ? ", organizationID))
}

func (c *organizationRepo) GetAddressByUUID(contactUUID string) (entity.OrganizationAddress, error) {
	return c.parseAddressEntity(c.conn.QueryRow(c.getAddressQuery()+" WHERE a.organization_address_uuid = ? ", contactUUID))
}

func (c *organizationRepo) SetParentOrganization(organizationID int64, parentOrganizationID int64) error {
	const (
		query = `UPDATE tab_organization SET organization_parent_id = ? WHERE organization_id = ?;`
	)

	_, err := c.conn.Exec(query, parentOrganizationID, organizationID)
	if err != nil {
		return err
	} else {
		return nil
	}
}

func (c *organizationRepo) InactivateOrganizationAddress(address entity.OrganizationAddress, user entity.User) error {
	const (
		query = "UPDATE tab_organization_address SET active = 0, updated = CURRENT_TIMESTAMP, updated_user_id = ? WHERE organization_id = ? and organization_address_uuid = ?"
	)

	if address.Organization == nil {
		return errors.NewNotFoundError()
	}

	organization, err := c.GetByUUID(address.Organization.UUID, user)
	if err != nil {
		return err
	}

	_, err = c.conn.Exec(query, address.UpdatedUser.ID, organization.ID, address.UUID)
	if err != nil {
		return err
	} else {
		return nil
	}
}

func (c *organizationRepo) SetOrganizationAddress(address entity.OrganizationAddress, user entity.User) (entity.OrganizationAddress, error) {
	const (
		query = "INSERT INTO tab_organization_address(organization_address_uuid, organization_id, internal_id, `name`, address, `desc`, lat, `long`, created_user_id, updated_user_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
	)

	if address.Organization == nil {
		return entity.OrganizationAddress{}, errors.NewNotFoundError()
	}

	if address.Organization.ID == 0 {
		organization, err := c.GetByUUID(address.Organization.UUID, user)
		if err != nil {
			return entity.OrganizationAddress{}, err
		}
		address.Organization = &organization
	}

	UUID, _ := uuid.NewV4()
	result, err := c.conn.Exec(query, UUID.String(), address.Organization.ID, address.InternalID, address.Name, address.Address, address.Description, address.Latitude, address.Longitude, address.CreatedUser.ID, address.UpdatedUser.ID)
	if err != nil {
		return entity.OrganizationAddress{}, err
	}

	address.ID, err = result.LastInsertId()
	if err != nil {
		return address, err
	}

	return address, nil
}

func (c *organizationRepo) InactivateOrganizationContact(contact entity.OrganizationContact, user entity.User) error {
	const (
		query = "UPDATE tab_organization_contact SET active = 0, updated = CURRENT_TIMESTAMP, updated_user_id = ? WHERE organization_id = ? and organization_contact_uuid = ?"
	)

	if contact.Organization == nil {
		return errors.NewNotFoundError()
	}

	organization, err := c.GetByUUID(contact.Organization.UUID, user)
	if err != nil {
		return err
	}

	_, err = c.conn.Exec(query, contact.UpdatedUser.ID, organization.ID, contact.UUID)
	if err != nil {
		return err
	} else {
		return nil
	}
}

func (c *organizationRepo) SetOrganizationContact(contact entity.OrganizationContact, user entity.User) (entity.OrganizationContact, error) {
	const (
		selectQuery = "SELECT a.contact_type_id, a.name, a.key FROM tab_contact_type a WHERE a.key = ?"
		query       = "INSERT INTO tab_organization_contact(organization_contact_uuid, organization_id, contact_type_id, contact, contact_name, contact_desc, created_user_id, updated_user_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?);"
	)

	if contact.Organization == nil {
		return entity.OrganizationContact{}, errors.NewNotFoundError()
	}

	organization, err := c.GetByUUID(contact.Organization.UUID, user)
	if err != nil {
		return entity.OrganizationContact{}, err
	}

	row := c.conn.QueryRow(selectQuery, contact.Type.Key)
	err = row.Scan(&contact.Type.ID, &contact.Type.Value, &contact.Type.Key)
	if err != nil {
		return entity.OrganizationContact{}, err
	}

	UUID, _ := uuid.NewV4()
	result, err := c.conn.Exec(query, UUID.String(), organization.ID, contact.Type.ID, contact.Contact, contact.Name, contact.Description, contact.CreatedUser.ID, contact.UpdatedUser.ID)
	if err != nil {
		return entity.OrganizationContact{}, err
	}

	contact.ID, err = result.LastInsertId()
	if err != nil {
		return contact, err
	}

	return contact, nil
}

func (c *organizationRepo) AddOrganization(organization entity.Organization, user entity.User) (entity.Organization, error) {
	const (
		queryOrgType   = "SELECT a.organization_type_id FROM tab_organization_type a WHERE a.organization_type_key = ?;"
		queryParentOrg = "SELECT a.organization_id FROM tab_organization a WHERE a.organization_uuid = ?;"
		query          = "INSERT INTO tab_organization(organization_uuid, organization_type_id, organization_name, organization_description, organization_reference_id, organization_parent_id, main_organization, created_user, updated_user) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?);"
	)

	row := c.conn.QueryRow(queryOrgType, organization.Type.Key)
	err := row.Scan(&organization.Type.ID)
	if err != nil {
		return entity.Organization{}, err
	}

	if organization.Parent != nil && organization.Parent.UUID != "" {
		row := c.conn.QueryRow(queryParentOrg, organization.Parent.UUID)
		err := row.Scan(&organization.Parent.ID)
		if err != nil {
			return entity.Organization{}, err
		}
		organization.ParentID = organization.Parent.ID
	}

	UUID, _ := uuid.NewV4()
	organization.UUID = UUID.String()
	result, err := c.conn.Exec(query, organization.UUID, organization.Type.ID, organization.Name, organization.Description, organization.ReferenceID, organization.ParentID, false, organization.Author.ID, organization.LastEditor.ID)
	if err != nil {
		return entity.Organization{}, err
	}

	ID, err := result.LastInsertId()
	if err != nil {
		return entity.Organization{}, err
	}

	organization.ID = ID
	if len(organization.Addresses) > 0 {
		for i, a := range organization.Addresses {
			a.Organization = &organization
			address, err := c.SetOrganizationAddress(a, user)
			if err != nil {
				fmt.Println("Error to save addresses")
				return entity.Organization{}, err
			}
			organization.Addresses[i] = address
		}
	}

	if len(organization.Contacts) > 0 {
		for i, ct := range organization.Contacts {
			ct.Organization = &organization
			contact, err := c.SetOrganizationContact(ct, user)
			if err != nil {
				fmt.Println("Error to save contacts")
				return entity.Organization{}, err
			}
			organization.Contacts[i] = contact
		}
	}

	return organization, nil
}
