package database

import (
	"context"
	"database/sql"
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/crypto/bcrypt"
	"gopkg.in/irc.v4"
)

type MessageTarget struct {
	Name          string
	LatestMessage time.Time
}

type MessageOptions struct {
	AfterID    int64
	AfterTime  time.Time
	BeforeTime time.Time
	Limit      int
	Events     bool
	Sender     string
	Text       string
	TakeLast   bool
}

type Database interface {
	Close() error
	Stats(ctx context.Context) (*DatabaseStats, error)

	ListUsers(ctx context.Context) ([]User, error)
	GetUser(ctx context.Context, username string) (*User, error)
	StoreUser(ctx context.Context, user *User) error
	DeleteUser(ctx context.Context, id int64) error
	ListInactiveUsernames(ctx context.Context, limit time.Time) ([]string, error)

	ListNetworks(ctx context.Context, userID int64) ([]Network, error)
	StoreNetwork(ctx context.Context, userID int64, network *Network) error
	DeleteNetwork(ctx context.Context, id int64) error
	ListChannels(ctx context.Context, networkID int64) ([]Channel, error)
	StoreChannel(ctx context.Context, networKID int64, ch *Channel) error
	DeleteChannel(ctx context.Context, id int64) error

	ListDeliveryReceipts(ctx context.Context, networkID int64) ([]DeliveryReceipt, error)
	StoreClientDeliveryReceipts(ctx context.Context, networkID int64, client string, receipts []DeliveryReceipt) error

	GetReadReceipt(ctx context.Context, networkID int64, name string) (*ReadReceipt, error)
	StoreReadReceipt(ctx context.Context, networkID int64, receipt *ReadReceipt) error

	ListWebPushConfigs(ctx context.Context) ([]WebPushConfig, error)
	StoreWebPushConfig(ctx context.Context, config *WebPushConfig) error

	ListWebPushSubscriptions(ctx context.Context, userID, networkID int64) ([]WebPushSubscription, error)
	StoreWebPushSubscription(ctx context.Context, userID, networkID int64, sub *WebPushSubscription) error
	DeleteWebPushSubscription(ctx context.Context, id int64) error

	GetMessageLastID(ctx context.Context, networkID int64, name string) (int64, error)
	StoreMessage(ctx context.Context, networkID int64, name string, msg *irc.Message) (int64, error)
	ListMessageLastPerTarget(ctx context.Context, networkID int64, options *MessageOptions) ([]MessageTarget, error)
	ListMessages(ctx context.Context, networkID int64, name string, options *MessageOptions) ([]*irc.Message, error)
}

type MetricsCollectorDatabase interface {
	Database
	RegisterMetrics(r prometheus.Registerer) error
}

func Open(driver, source string) (Database, error) {
	switch driver {
	case "sqlite3":
		return OpenSqliteDB(source)
	case "postgres":
		return OpenPostgresDB(source)
	default:
		return nil, fmt.Errorf("unsupported database driver: %q", driver)
	}
}

type DatabaseStats struct {
	Users    int64
	Networks int64
	Channels int64
}

type User struct {
	ID                     int64
	Username               string
	Password               string // hashed
	Nick                   string
	Realname               string
	Admin                  bool
	Enabled                bool
	DownstreamInteractedAt time.Time
}

func (u *User) CheckPassword(password string) (upgraded bool, err error) {
	// Password auth disabled
	if u.Password == "" {
		return false, fmt.Errorf("password auth disabled")
	}

	err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
	if err != nil {
		return false, fmt.Errorf("wrong password: %v", err)
	}

	passCost, err := bcrypt.Cost([]byte(u.Password))
	if err != nil {
		return false, fmt.Errorf("invalid password cost: %v", err)
	}

	if passCost < bcrypt.DefaultCost {
		return true, u.SetPassword(password)
	}
	return false, nil
}

func (u *User) SetPassword(password string) error {
	hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return fmt.Errorf("failed to hash password: %v", err)
	}
	u.Password = string(hashed)
	return nil
}

type SASL struct {
	Mechanism string

	Plain struct {
		Username string
		Password string
	}

	// TLS client certificate authentication.
	External struct {
		// X.509 certificate in DER form.
		CertBlob []byte
		// PKCS#8 private key in DER form.
		PrivKeyBlob []byte
	}
}

type Network struct {
	ID              int64
	Name            string
	Addr            string
	Nick            string
	Username        string
	Realname        string
	Pass            string
	ConnectCommands []string
	CertFP          string
	SASL            SASL
	AutoAway        bool
	Enabled         bool
}

func (net *Network) GetName() string {
	if net.Name != "" {
		return net.Name
	}
	return net.Addr
}

func (net *Network) URL() (*url.URL, error) {
	s := net.Addr
	if !strings.Contains(s, "://") {
		// This is a raw domain name, make it an URL with the default scheme
		s = "ircs://" + s
	}

	u, err := url.Parse(s)
	if err != nil {
		return nil, fmt.Errorf("failed to parse upstream server URL: %v", err)
	}

	return u, nil
}

func GetNick(user *User, net *Network) string {
	if net != nil && net.Nick != "" {
		return net.Nick
	}
	if user.Nick != "" {
		return user.Nick
	}
	return user.Username
}

func GetUsername(user *User, net *Network) string {
	if net != nil && net.Username != "" {
		return net.Username
	}
	return GetNick(user, net)
}

func GetRealname(user *User, net *Network) string {
	if net != nil && net.Realname != "" {
		return net.Realname
	}
	if user.Realname != "" {
		return user.Realname
	}
	return GetNick(user, net)
}

type MessageFilter int

const (
	// TODO: use customizable user defaults for FilterDefault
	FilterDefault MessageFilter = iota
	FilterNone
	FilterHighlight
	FilterMessage
)

type Channel struct {
	ID   int64
	Name string
	Key  string

	Detached              bool
	DetachedInternalMsgID string

	RelayDetached MessageFilter
	ReattachOn    MessageFilter
	DetachAfter   time.Duration
	DetachOn      MessageFilter
}

type DeliveryReceipt struct {
	ID            int64
	Target        string // channel or nick
	Client        string
	InternalMsgID string
}

type ReadReceipt struct {
	ID        int64
	Target    string // channel or nick
	Timestamp time.Time
}

type WebPushConfig struct {
	ID        int64
	VAPIDKeys struct {
		Public, Private string
	}
}

type WebPushSubscription struct {
	ID                   int64
	Endpoint             string
	CreatedAt, UpdatedAt time.Time // read-only

	Keys struct {
		Auth   string
		P256DH string
		VAPID  string
	}
}

func toNullString(s string) sql.NullString {
	return sql.NullString{
		String: s,
		Valid:  s != "",
	}
}

func toNullTime(t time.Time) sql.NullTime {
	return sql.NullTime{
		Time:  t,
		Valid: !t.IsZero(),
	}
}
