package main

import (
	"errors"
	"net"

	"github.com/Queueue0/qpass/internal/crypto"
	"github.com/Queueue0/qpass/internal/protocol"
	"github.com/google/uuid"
)

var (
	ErrNoActiveUser = errors.New("no logged in user")
	ErrPingFail     = errors.New("Unable to ping sync server")
	ErrCommFail     = errors.New("Communication with server failed unexpectedly")
)

func (app *Application) send(p *protocol.Payload) error {
	c, err := net.Dial("tcp", app.ServerAddress())
	if err != nil {
		return err
	}

	defer c.Close()
	_, err = p.WriteTo(c)

	return err
}

func (app *Application) sync() error {
	if app.ActiveUser == nil {
		return ErrNoActiveUser
	}

	activeUUID := app.ActiveUser.ID.String()

	ad := protocol.AuthData{Token: app.ActiveUser.AuthToken}
	authBytes, err := ad.Encode()
	if err != nil {
		return err
	}

	apl, err := protocol.NewPayload(protocol.AUTH, authBytes)
	if err != nil {
		return err
	}

	c, err := crypto.Dial(app.ServerAddress())
	if err != nil {
		return err
	}
	defer func() {
		protocol.NewSucc().WriteTo(c)
		c.Close()
	}()

	_, err = apl.WriteTo(c)
	if err != nil {
		return err
	}

	authResp := protocol.Payload{}
	_, err = authResp.ReadFrom(c)
	if err != nil {
		return err
	}

	if authResp.Type() == protocol.FAIL {
		// Try to add new user, then retry auth
		_, err := app.newUserSync(app.ActiveUser.ID.String(), app.ActiveUser.AuthToken)
		if err != nil {
			return err
		}

		_, err = apl.WriteTo(c)
		if err != nil {
			return err
		}

		authResp = protocol.Payload{}
		_, err = authResp.ReadFrom(c)
		if err != nil {
			return err
		}

		if authResp.Type() == protocol.FAIL {
			return errors.New("Remote error: " + authResp.String())
		}
	}

	if authResp.Type() != protocol.SUCC {
		return errors.New("Unexpected response type from server")
	}

	pws, err := app.PasswordModel.GetAllEncryptedForUser(*app.ActiveUser)
	if err != nil {
		return err
	}

	sd := protocol.SyncData{UUID: activeUUID, Passwords: pws}
	bytes, err := sd.Encode()
	if err != nil {
		return err
	}

	pl, err := protocol.NewPayload(protocol.SYNC, bytes)
	if err != nil {
		return err
	}

	_, err = pl.WriteTo(c)
	if err != nil {
		return err
	}

	r := protocol.Payload{}
	_, err = r.ReadFrom(c)
	if err != nil {
		return err
	}
	if r.Type() == protocol.FAIL {
		return errors.New("Remote error: " + r.String())
	}

	rd := protocol.SyncData{}
	err = rd.Decode(r.Bytes())
	if err != nil {
		return err
	}

	err = app.PasswordModel.ReplaceAllForUser(app.ActiveUser.ID.String(), rd.Passwords)
	if err != nil {
		return err
	}

	return nil
}

func (app *Application) newUserSync(id string, authToken []byte) (string, error) {
	nud := protocol.NewUserData{Token: authToken}
	if id != "" {
		nud.UUID = id
	}

	b, err := nud.Encode()
	if err != nil {
		return "", err
	}

	c, err := crypto.Dial(app.ServerAddress())
	if err != nil {
		return "", ErrPingFail
	}
	defer c.Close()

	protocol.NewPing().WriteTo(c)
	r := protocol.Payload{}
	r.ReadFrom(c)

	if r.Type() != protocol.PONG {
		return "", ErrPingFail
	}

	// Will never error here, only possible error is if max payload size is
	// exceeded which won't happen with just an auth token
	p, _ := protocol.NewPayload(protocol.NUSR, b)
	p.WriteTo(c)

	r = protocol.Payload{}
	r.ReadFrom(c)

	if r.Type() == protocol.FAIL {
		return "", errors.New(string(r.Bytes()))
	}

	if r.Type() != protocol.SUCC {
		return "", ErrCommFail
	}

	newid := string(r.Bytes())
	_, err = uuid.Parse(newid)

	protocol.NewSucc().WriteTo(c)
	return newid, err
}

func (app *Application) loginSync(username, password string) error {
	// Try to authenticate first
	u, err := app.UserModel.Authenticate(username, password)
	if err == nil {
		// Should maybe make a call to sync in this block
		app.ActiveUser = &u
		return nil
	}

	ad := protocol.AuthData{Token: crypto.ClientAuthToken(username, password)}
	authBytes, err := ad.Encode()
	if err != nil {
		return err
	}

	apl, err := protocol.NewPayload(protocol.AUTH, authBytes)
	if err != nil {
		return err
	}

	c, err := crypto.Dial(app.ServerAddress())
	if err != nil {
		return err
	}
	defer func() {
		protocol.NewSucc().WriteTo(c)
		c.Close()
	}()

	_, err = apl.WriteTo(c)
	if err != nil {
		return err
	}

	authResp := protocol.Payload{}
	_, err = authResp.ReadFrom(c)
	if err != nil {
		return err
	}

	if authResp.Type() == protocol.FAIL {
		protocol.NewSucc().WriteTo(c)
		return errors.New("Remote error: " + authResp.String())
	}

	if authResp.Type() != protocol.SUCC {
		return errors.New("Unexpected response type from server")
	}

	idStr := string(authResp.Bytes())
	_, err = app.UserModel.Insert(username, password, idStr)
	if err != nil {
		return err
	}

	u, err = app.UserModel.Authenticate(username, password)
	if err != nil {
		return err
	}
	app.ActiveUser = &u

	err = app.sync()
	if err != nil {
		return errors.New("sync: " + err.Error())
	}

	return nil
}