// Copyright (c) 2012-present The upper.io/db authors. All rights reserved.
//
// 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 provides an adapter for PostgreSQL.
// See https://github.com/upper/db/adapter/postgresql for documentation,
// particularities and usage examples.
package postgresql

import (
	"fmt"
	"strings"

	db "github.com/upper/db/v4"
	"github.com/upper/db/v4/internal/sqladapter"
	"github.com/upper/db/v4/internal/sqladapter/exql"
	"github.com/upper/db/v4/internal/sqlbuilder"
)

type database struct {
}

func (*database) Template() *exql.Template {
	return template
}

func (*database) Collections(sess sqladapter.Session) (collections []string, err error) {
	q := sess.SQL().
		Select("table_name").
		From("information_schema.tables").
		Where("table_schema = ?", "public")

	iter := q.Iterator()
	defer iter.Close()

	for iter.Next() {
		var name string
		if err := iter.Scan(&name); err != nil {
			return nil, err
		}
		collections = append(collections, name)
	}
	if err := iter.Err(); err != nil {
		return nil, err
	}

	return collections, nil
}

func (*database) ConvertValue(in interface{}) interface{} {
	switch v := in.(type) {
	case *[]int64:
		return (*Int64Array)(v)
	case *[]string:
		return (*StringArray)(v)
	case *[]float64:
		return (*Float64Array)(v)
	case *[]bool:
		return (*BoolArray)(v)
	case *map[string]interface{}:
		return (*JSONBMap)(v)

	case []int64:
		return (*Int64Array)(&v)
	case []string:
		return (*StringArray)(&v)
	case []float64:
		return (*Float64Array)(&v)
	case []bool:
		return (*BoolArray)(&v)
	case map[string]interface{}:
		return (*JSONBMap)(&v)

	}
	return in
}

func (*database) CompileStatement(sess sqladapter.Session, stmt *exql.Statement, args []interface{}) (string, []interface{}, error) {
	compiled, err := stmt.Compile(template)
	if err != nil {
		return "", nil, err
	}

	query, args := sqlbuilder.Preprocess(compiled, args)
	query = sqladapter.ReplaceWithDollarSign(query)
	return query, args, nil
}

func (*database) Err(err error) error {
	if err != nil {
		s := err.Error()
		// These errors are not exported so we have to check them by they string value.
		if strings.Contains(s, `too many clients`) || strings.Contains(s, `remaining connection slots are reserved`) || strings.Contains(s, `too many open`) {
			return db.ErrTooManyClients
		}
	}
	return err
}

func (*database) NewCollection() sqladapter.CollectionAdapter {
	return &collectionAdapter{}
}

func (*database) LookupName(sess sqladapter.Session) (string, error) {
	q := sess.SQL().
		Select(db.Raw("CURRENT_DATABASE() AS name"))

	iter := q.Iterator()
	defer iter.Close()

	if iter.Next() {
		var name string
		if err := iter.Scan(&name); err != nil {
			return "", err
		}
		return name, nil
	}

	return "", iter.Err()
}

func (*database) TableExists(sess sqladapter.Session, name string) error {
	q := sess.SQL().
		Select("table_name").
		From("information_schema.tables").
		Where("table_catalog = ? AND table_name = ?", sess.Name(), name)

	iter := q.Iterator()
	defer iter.Close()

	if iter.Next() {
		var name string
		if err := iter.Scan(&name); err != nil {
			return err
		}
		return nil
	}
	if err := iter.Err(); err != nil {
		return err
	}

	return db.ErrCollectionDoesNotExist
}

func (*database) PrimaryKeys(sess sqladapter.Session, tableName string) ([]string, error) {
	q := sess.SQL().
		Select("pg_attribute.attname AS pkey").
		From("pg_index", "pg_class", "pg_attribute").
		Where(`
			pg_class.oid = '` + quotedTableName(tableName) + `'::regclass
			AND indrelid = pg_class.oid
			AND pg_attribute.attrelid = pg_class.oid
			AND pg_attribute.attnum = ANY(pg_index.indkey)
			AND indisprimary
		`).OrderBy("pkey")

	iter := q.Iterator()
	defer iter.Close()

	pk := []string{}

	for iter.Next() {
		var k string
		if err := iter.Scan(&k); err != nil {
			return nil, err
		}
		pk = append(pk, k)
	}
	if err := iter.Err(); err != nil {
		return nil, err
	}

	return pk, nil
}

// quotedTableName returns a valid regclass name for both regular tables and
// for schemas.
func quotedTableName(s string) string {
	chunks := strings.Split(s, ".")
	for i := range chunks {
		chunks[i] = fmt.Sprintf("%q", chunks[i])
	}
	return strings.Join(chunks, ".")
}
