package auth

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/hex"
	"errors"
	"time"
)

var ErrNotFound = errors.New("auth: not found")

type UserRepo interface {
	FindByEmail(ctx context.Context, email string) (*User, error)
	Upsert(ctx context.Context, u *User) (*User, error)
}

type SchoolRepo interface {
	FindByCode(ctx context.Context, code string) (*School, error)
	Insert(ctx context.Context, s *School) (*School, error)
}

type MySQLStore struct {
	db *sql.DB
}

func NewMySQLStore(db *sql.DB) *MySQLStore {
	return &MySQLStore{db: db}
}

func (s *MySQLStore) FindByEmail(ctx context.Context, email string) (*User, error) {
	const q = `SELECT id, username, fullname, email, img_url, provider, role,
		user_state, user_ppd, user_school, user_class_year, user_class_name, login_token
		FROM frontend_user WHERE email = ? OR username = ? ORDER BY id LIMIT 1`
	row := s.db.QueryRowContext(ctx, q, email, email)

	u := &User{}
	var imgURL, provider, role, state, ppd, school, classYear, className, loginToken sql.NullString
	err := row.Scan(&u.ID, &u.Username, &u.Fullname, &u.Email, &imgURL, &provider, &role,
		&state, &ppd, &school, &classYear, &className, &loginToken)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, err
	}
	u.ImgURL = imgURL.String
	u.Provider = provider.String
	u.Role = role.String
	u.State = state.String
	u.PPD = ppd.String
	u.School = school.String
	u.ClassYear = classYear.String
	u.ClassName = className.String
	u.LoginToken = loginToken.String
	return u, nil
}

func (s *MySQLStore) Upsert(ctx context.Context, u *User) (*User, error) {
	existing, err := s.FindByEmail(ctx, u.Email)
	if err != nil && !errors.Is(err, ErrNotFound) {
		return nil, err
	}

	now := time.Now()
	if existing == nil {
		if u.LoginToken == "" {
			u.LoginToken = newToken()
		}
		const q = `INSERT INTO frontend_user
			(username, fullname, email, img_url, provider, role, user_state, user_ppd,
			 user_school, user_class_year, user_class_name, login_token, status,
			 last_login, created_at, updated_at)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)`
		res, err := s.db.ExecContext(ctx, q, u.Username, u.Fullname, u.Email, u.ImgURL,
			u.Provider, u.Role, u.State, u.PPD, u.School, u.ClassYear, u.ClassName,
			u.LoginToken, now.Unix(), now, now)
		if err != nil {
			return nil, err
		}
		u.ID, _ = res.LastInsertId()
		return u, nil
	}

	u.ID = existing.ID
	u.LoginToken = existing.LoginToken
	const q = `UPDATE frontend_user SET fullname = ?, email = ?, img_url = ?, provider = ?,
		role = ?, user_state = ?, user_ppd = ?, user_school = ?, user_class_year = ?,
		user_class_name = ?, last_login = ?, updated_at = ? WHERE id = ?`
	_, err = s.db.ExecContext(ctx, q, u.Fullname, u.Email, u.ImgURL, u.Provider,
		u.Role, u.State, u.PPD, u.School, u.ClassYear, u.ClassName, now.Unix(), now, u.ID)
	if err != nil {
		return nil, err
	}
	return u, nil
}

func (s *MySQLStore) FindByCode(ctx context.Context, code string) (*School, error) {
	const q = `SELECT id, state, ppd, type, school_code, name FROM school WHERE school_code = ? LIMIT 1`
	row := s.db.QueryRowContext(ctx, q, code)

	sc := &School{}
	var state, ppd, typ, name sql.NullString
	err := row.Scan(&sc.ID, &state, &ppd, &typ, &sc.Code, &name)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, err
	}
	sc.State = state.String
	sc.PPD = ppd.String
	sc.Type = typ.String
	sc.Name = name.String
	return sc, nil
}

func (s *MySQLStore) Insert(ctx context.Context, sc *School) (*School, error) {
	now := time.Now()
	const q = `INSERT INTO school
		(school_code, name, email, address, city, postcode, ppd, state, type, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
	res, err := s.db.ExecContext(ctx, q, sc.Code, sc.Name, sc.Email, sc.Address, sc.City,
		sc.Postcode, sc.PPD, sc.State, sc.Type, now, now)
	if err != nil {
		return nil, err
	}
	sc.ID, _ = res.LastInsertId()
	return sc, nil
}

func newToken() string {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return time.Now().UTC().Format("20060102150405.000000000")
	}
	return hex.EncodeToString(b)
}

type MemoryStore struct {
	users   map[string]*User
	schools map[string]*School
	seq     int64
}

func NewMemoryStore() *MemoryStore {
	return &MemoryStore{users: map[string]*User{}, schools: map[string]*School{}}
}

func (s *MemoryStore) FindByEmail(_ context.Context, email string) (*User, error) {
	if u, ok := s.users[email]; ok {
		clone := *u
		return &clone, nil
	}
	return nil, ErrNotFound
}

func (s *MemoryStore) Upsert(_ context.Context, u *User) (*User, error) {
	if existing, ok := s.users[u.Email]; ok {
		u.ID = existing.ID
		if u.LoginToken == "" {
			u.LoginToken = existing.LoginToken
		}
	}
	if u.ID == 0 {
		s.seq++
		u.ID = s.seq
	}
	if u.LoginToken == "" {
		u.LoginToken = newToken()
	}
	clone := *u
	s.users[u.Email] = &clone
	return u, nil
}

func (s *MemoryStore) FindByCode(_ context.Context, code string) (*School, error) {
	if sc, ok := s.schools[code]; ok {
		clone := *sc
		return &clone, nil
	}
	return nil, ErrNotFound
}

func (s *MemoryStore) Insert(_ context.Context, sc *School) (*School, error) {
	s.seq++
	sc.ID = s.seq
	clone := *sc
	s.schools[sc.Code] = &clone
	return sc, nil
}
