package hr

import (
	"context"
	"errors"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/shopspring/decimal"

	"github.com/TaruLab/keluargamart-backend/internal/db"
)

// Repository implements Store on a pgx pool via sqlc.
type Repository struct {
	pool *pgxpool.Pool
	q    *db.Queries
}

func NewRepository(pool *pgxpool.Pool) *Repository {
	return &Repository{pool: pool, q: db.New(pool)}
}

// --- employees ---------------------------------------------------------------

func (r *Repository) ListEmployees(ctx context.Context, f Filter, search string, active *bool) ([]db.ListEmployeesRow, int64, error) {
	total, err := r.q.CountEmployees(ctx, db.CountEmployeesParams{
		Search: textOrNil(search),
		Active: boolOrNil(active),
	})
	if err != nil {
		return nil, 0, err
	}
	page, per := normPage(f.Page, f.PerPage)
	rows, err := r.q.ListEmployees(ctx, db.ListEmployeesParams{
		Limit:  int32(per),
		Offset: int32((page - 1) * per),
		Search: textOrNil(search),
		Active: boolOrNil(active),
		SortBy: textOrNil(f.SortBy),
		SortDir: textOrNil(f.SortDir),
	})
	if err != nil {
		return nil, 0, err
	}
	return rows, total, nil
}

func (r *Repository) EmployeeByID(ctx context.Context, id int64) (db.GetEmployeeByIDRow, error) {
	row, err := r.q.GetEmployeeByID(ctx, id)
	return row, mapErr(err)
}

func (r *Repository) EmployeeByUserID(ctx context.Context, userID int64) (db.GetEmployeeByUserIDRow, error) {
	row, err := r.q.GetEmployeeByUserID(ctx, int8(userID))
	return row, mapErr(err)
}

func (r *Repository) CreateEmployee(ctx context.Context, p db.CreateEmployeeParams) (db.CreateEmployeeRow, error) {
	row, err := r.q.CreateEmployee(ctx, p)
	if err != nil {
		return db.CreateEmployeeRow{}, mapEmployeeInsertErr(err)
	}
	return row, nil
}

func (r *Repository) UpdateEmployee(ctx context.Context, p db.UpdateEmployeeParams) error {
	n, err := r.q.UpdateEmployee(ctx, p)
	if err != nil {
		return mapWriteErr(err)
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) DeleteEmployee(ctx context.Context, id int64) error {
	n, err := r.q.SoftDeleteEmployee(ctx, id)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

// --- attendance --------------------------------------------------------------

func (r *Repository) ListAttendance(ctx context.Context, f Filter, employeeID *int64) ([]db.ListAttendanceRow, int64, error) {
	total, err := r.q.CountAttendance(ctx, db.CountAttendanceParams{
		EmployeeID: int8OrNil(employeeID),
		Status:     textOrNil(f.Status),
		DateFrom:   dateOrNil(f.DateFrom),
		DateTo:     dateOrNil(f.DateTo),
	})
	if err != nil {
		return nil, 0, err
	}
	page, per := normPage(f.Page, f.PerPage)
	rows, err := r.q.ListAttendance(ctx, db.ListAttendanceParams{
		Limit:      int32(per),
		Offset:     int32((page - 1) * per),
		EmployeeID: int8OrNil(employeeID),
		Status:     textOrNil(f.Status),
		DateFrom:   dateOrNil(f.DateFrom),
		DateTo:     dateOrNil(f.DateTo),
	})
	if err != nil {
		return nil, 0, err
	}
	return rows, total, nil
}

func (r *Repository) AttendanceByID(ctx context.Context, id int64) (db.GetAttendanceByIDRow, error) {
	row, err := r.q.GetAttendanceByID(ctx, id)
	return row, mapErr(err)
}

func (r *Repository) AttendanceForDay(ctx context.Context, employeeID int64, date string) (db.GetAttendanceForDayRow, error) {
	row, err := r.q.GetAttendanceForDay(ctx, db.GetAttendanceForDayParams{
		EmployeeID:     employeeID,
		AttendanceDate: dateOrNil(date),
	})
	return row, mapErr(err)
}

func (r *Repository) CreateAttendance(ctx context.Context, p db.CreateAttendanceParams) (db.CreateAttendanceRow, error) {
	return r.q.CreateAttendance(ctx, p)
}

func (r *Repository) UpdateAttendance(ctx context.Context, p db.UpdateAttendanceParams) error {
	n, err := r.q.UpdateAttendance(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) UpsertLeaveAttendance(ctx context.Context, p db.UpsertLeaveAttendanceParams) error {
	return r.q.UpsertLeaveAttendance(ctx, p)
}

func (r *Repository) ListAttendanceForEmployeePeriod(ctx context.Context, p db.ListAttendanceForEmployeePeriodParams) ([]db.ListAttendanceForEmployeePeriodRow, error) {
	return r.q.ListAttendanceForEmployeePeriod(ctx, p)
}

func (r *Repository) ListApprovedLeavesForEmployeePeriod(ctx context.Context, p db.ListApprovedLeavesForEmployeePeriodParams) ([]db.ListApprovedLeavesForEmployeePeriodRow, error) {
	return r.q.ListApprovedLeavesForEmployeePeriod(ctx, p)
}

// --- leaves ------------------------------------------------------------------

func (r *Repository) ListLeaves(ctx context.Context, f Filter, employeeID *int64) ([]db.ListLeavesRow, int64, error) {
	total, err := r.q.CountLeaves(ctx, db.CountLeavesParams{
		EmployeeID: int8OrNil(employeeID),
		Status:     textOrNil(f.Status),
		DateFrom:   dateOrNil(f.DateFrom),
		DateTo:     dateOrNil(f.DateTo),
	})
	if err != nil {
		return nil, 0, err
	}
	page, per := normPage(f.Page, f.PerPage)
	rows, err := r.q.ListLeaves(ctx, db.ListLeavesParams{
		Limit:      int32(per),
		Offset:     int32((page - 1) * per),
		EmployeeID: int8OrNil(employeeID),
		Status:     textOrNil(f.Status),
		DateFrom:   dateOrNil(f.DateFrom),
		DateTo:     dateOrNil(f.DateTo),
	})
	if err != nil {
		return nil, 0, err
	}
	return rows, total, nil
}

func (r *Repository) LeaveByID(ctx context.Context, id int64) (db.GetLeaveByIDRow, error) {
	row, err := r.q.GetLeaveByID(ctx, id)
	return row, mapErr(err)
}

func (r *Repository) CreateLeave(ctx context.Context, p db.CreateLeaveParams) (db.CreateLeaveRow, error) {
	return r.q.CreateLeave(ctx, p)
}

func (r *Repository) UpdateLeave(ctx context.Context, p db.UpdateLeaveParams) error {
	n, err := r.q.UpdateLeave(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) DeductEmployeeLeave(ctx context.Context, p db.DeductEmployeeLeaveParams) (decimal.Decimal, error) {
	return r.q.DeductEmployeeLeave(ctx, p)
}

// --- payrolls ----------------------------------------------------------------

func (r *Repository) ListPayrolls(ctx context.Context, f Filter, employeeID *int64) ([]db.ListPayrollsRow, int64, error) {
	total, err := r.q.CountPayrolls(ctx, db.CountPayrollsParams{
		EmployeeID: int8OrNil(employeeID),
		Status:     textOrNil(f.Status),
	})
	if err != nil {
		return nil, 0, err
	}
	page, per := normPage(f.Page, f.PerPage)
	rows, err := r.q.ListPayrolls(ctx, db.ListPayrollsParams{
		Limit:      int32(per),
		Offset:     int32((page - 1) * per),
		EmployeeID: int8OrNil(employeeID),
		Status:     textOrNil(f.Status),
	})
	if err != nil {
		return nil, 0, err
	}
	return rows, total, nil
}

func (r *Repository) PayrollByID(ctx context.Context, id int64) (db.GetPayrollByIDRow, error) {
	row, err := r.q.GetPayrollByID(ctx, id)
	return row, mapErr(err)
}

func (r *Repository) CreatePayroll(ctx context.Context, p db.CreatePayrollParams) (db.CreatePayrollRow, error) {
	return r.q.CreatePayroll(ctx, p)
}

func (r *Repository) UpdatePayroll(ctx context.Context, p db.UpdatePayrollParams) error {
	n, err := r.q.UpdatePayroll(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) UpsertPayroll(ctx context.Context, p db.UpsertPayrollParams) (db.UpsertPayrollRow, error) {
	row, err := r.q.UpsertPayroll(ctx, p)
	return row, mapErr(err)
}

func (r *Repository) PayPayroll(ctx context.Context, p db.PayPayrollParams) error {
	n, err := r.q.PayPayroll(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) ActiveEmployees(ctx context.Context) ([]db.ListActiveEmployeesRow, error) {
	return r.q.ListActiveEmployees(ctx)
}

// --- performances ------------------------------------------------------------

func (r *Repository) ListPerformances(ctx context.Context, f Filter) ([]db.ListPerformancesRow, int64, error) {
	total, err := r.q.CountPerformances(ctx)
	if err != nil {
		return nil, 0, err
	}
	page, per := normPage(f.Page, f.PerPage)
	rows, err := r.q.ListPerformances(ctx, db.ListPerformancesParams{
		Limit:  int32(per),
		Offset: int32((page - 1) * per),
	})
	if err != nil {
		return nil, 0, err
	}
	return rows, total, nil
}

func (r *Repository) CreatePerformance(ctx context.Context, p db.CreatePerformanceParams) (db.CreatePerformanceRow, error) {
	return r.q.CreatePerformance(ctx, p)
}

func (r *Repository) GetLatestEmployeeNumber(ctx context.Context) (string, error) {
	return r.q.GetLatestEmployeeNumber(ctx)
}

func (r *Repository) PerformanceByID(ctx context.Context, id int64) (db.GetPerformanceByIDRow, error) {
	row, err := r.q.GetPerformanceByID(ctx, id)
	return row, mapErr(err)
}

func (r *Repository) UpdatePerformance(ctx context.Context, p db.UpdatePerformanceParams) error {
	n, err := r.q.UpdatePerformance(ctx, p)
	if err != nil {
		return mapWriteErr(err)
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) DeletePerformance(ctx context.Context, id int64) error {
	n, err := r.q.DeletePerformance(ctx, id)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

// --- helpers -----------------------------------------------------------------

func mapErr(err error) error {
	if errors.Is(err, pgx.ErrNoRows) {
		return ErrNotFound
	}
	return err
}

func mapWriteErr(err error) error {
	if err == nil {
		return nil
	}
	var pge *pgconn.PgError
	if errors.As(err, &pge) && pge.Code == "23505" {
		return ErrDuplicate
	}
	return err
}

// mapEmployeeInsertErr splits the two unique violations an employee insert can
// hit, because they mean opposite things to the person submitting the form.
//
// The table has two unique columns (000001_init_schema.sql:45-47):
//
//   - employees_employee_number_key - the number is ALLOCATED BY THE SERVER
//     when the form leaves it blank. A violation here is our bug: the caller
//     sent no employee number, so there is nothing they can change, and
//     answering "that value is already in use" would send an HR clerk hunting
//     for a duplicate they never typed. It becomes a server fault.
//   - employees_nik_key - the NIK is supplied by the caller. A violation is a
//     genuine duplicate the caller CAN act on, and is the case the 23505
//     message was written for.
//
// This is why the generic 23505 message in pkg/httpresponse cannot be trusted
// for every caller: the shared layer cannot know which column collided, so the
// distinction has to be made here, where the schema is known.
func mapEmployeeInsertErr(err error) error {
	if err == nil {
		return nil
	}
	var pge *pgconn.PgError
	if !errors.As(err, &pge) || pge.Code != "23505" {
		return err
	}
	if pge.ConstraintName == "employees_employee_number_key" {
		return ErrNumberAllocation
	}
	return ErrDuplicate
}

func normPage(page, perPage int) (int, int) {
	if page < 1 {
		page = 1
	}
	if perPage < 1 || perPage > 100 {
		perPage = 20
	}
	return page, perPage
}

// --- pgtype helpers ----------------------------------------------------------

func int8(v int64) pgtype.Int8 {
	return pgtype.Int8{Int64: v, Valid: true}
}

func boolOrNil(b *bool) pgtype.Bool {
	if b == nil {
		return pgtype.Bool{}
	}
	return pgtype.Bool{Bool: *b, Valid: true}
}
