package payroll

import (
	"business-engine/internal/database"
	excelread "business-engine/internal/excel_read"
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/jackc/pgx/v5/pgtype"
)

type Repository interface {
	ListActiveEmployees(ctx context.Context) ([]database.ListActiveEmployeesRow, error)
	GetWorkSummaryForEmployeeInPeriod(ctx context.Context, args database.GetWorkSummaryForEmployeeInPeriodParams) (database.GetWorkSummaryForEmployeeInPeriodRow, error)
	GetSicknessSummaryForEmployeeInPeriod(ctx context.Context, args database.GetSicknessSummaryForEmployeeInPeriodParams) (database.GetSicknessSummaryForEmployeeInPeriodRow, error)
	ListWorkTimesForEmployeeInPeriod(ctx context.Context, args database.ListWorkTimesForEmployeeInPeriodParams) ([]database.WorkTime, error)
	ListPlannedTimesForEmployeeInPeriod(ctx context.Context, args database.ListPlannedTimesForEmployeeInPeriodParams) ([]database.ListPlannedTimesForEmployeeInPeriodRow, error)
	FetchDrivenToursInInterval(ctx context.Context, args database.FetchDrivenToursInIntervalParams) ([]database.Tour, error)
	CountTourengeldAmountInPeriod(ctx context.Context, args database.CountTourengeldAmountInPeriodParams) (float64, error)
	GetEmployeeByName(ctx context.Context, args database.GetEmployeeByNameParams) (database.GetEmployeeByNameRow, error)
}

type Engine interface {
	Calculate() // payroll input -> payroll statement
}

type Service struct {
	repo   Repository
	engine Engine
	logger *slog.Logger
}

func NewService(repo Repository, logger *slog.Logger) *Service {
	return &Service{
		repo:   repo,
		logger: logger,
	}
}

func (s *Service) FetchPayrollInput(ctx context.Context, month time.Time, employeeFirstname, employeeLastname string) (excelread.PayrollInputData, error) {
	s.logger.Info("fetching payroll input", "month", month)

	employeeIDs, err := s.repo.GetEmployeeByName(ctx, database.GetEmployeeByNameParams{
		Firstname: employeeFirstname,
		Lastname:  employeeLastname,
	})
	if err != nil {
		s.logger.Error("could not find employee with that name", "firstname", employeeFirstname, "lastname", employeeLastname, "error", err)
		return excelread.PayrollInputData{}, err
	}
	s.logger.Debug("got employee data", "employee", employeeIDs)

	startOfMonth := BeginningOfMonth(month.AddDate(0, -1, 0))
	endOfMonth := EndOfMonth(month.AddDate(0, -1, 0))
	s.logger.Info("fetching data for interval", "startDate", startOfMonth, "endDate", endOfMonth)

	tourengeld, err := s.repo.CountTourengeldAmountInPeriod(ctx, database.CountTourengeldAmountInPeriodParams{
		StartDate: startOfMonth,
		EndDate:   endOfMonth,
		EmployeeID: pgtype.Text{
			String: employeeIDs.DispoliveID,
			Valid:  true,
		},
	})
	if err != nil {
		s.logger.Error("could not count tourengeld for employee", "employeeId", employeeIDs.DyflexisID, "error", err)
	}

	plannedtimes, err := s.repo.ListPlannedTimesForEmployeeInPeriod(ctx, database.ListPlannedTimesForEmployeeInPeriodParams{
		EmployeeID: fmt.Sprintf("%d", employeeIDs.DyflexisID),
		StartDate:  &startOfMonth,
		EndDate:    &endOfMonth,
	})
	if err != nil {
		s.logger.Error("could not fetch plannedtimes for employee", "employeeId", employeeIDs.DyflexisID, "error", err)
	}

	worktimes, err := s.repo.ListWorkTimesForEmployeeInPeriod(ctx, database.ListWorkTimesForEmployeeInPeriodParams{
		EmployeeID: fmt.Sprintf("%d", employeeIDs.DyflexisID),
		StartDate:  &startOfMonth,
		EndDate:    &endOfMonth,
	})
	if err != nil {
		s.logger.Error("could not fetch worktimes for employee", "employeeId", employeeIDs.DyflexisID, "error", err)
	}

	s.logger.Info("Fetched payroll input", "employee", fmt.Sprintf("%s %s", employeeFirstname, employeeLastname), "tourengeld", tourengeld, "workdays", len(worktimes))

	return excelread.NewPayrollInputData(employeeIDs.ContractSalaryHour, tourengeld, worktimes, plannedtimes), nil
}

func (s *Service) GenerateMonthlyRun(ctx context.Context, month time.Time) ([]PayrollInput, error) {
	s.logger.Info("starting monthly payroll run", "month", month)

	employees, err := s.repo.ListActiveEmployees(ctx)
	if err != nil {
		s.logger.Error("could not get active employees", "error", err)
		return []PayrollInput{}, err
	}
	s.logger.Info("found active employees for payroll run", "count", len(employees))

	var inputs []PayrollInput

	for _, employee := range employees {
		log := s.logger.With("employee_id", employee.ID)
		log.Info("processing employee")

		payrollInput := NewPayrollInput(employee)
		startOfMonth := BeginningOfMonth(month.AddDate(0, -1, 0))
		endOfMonth := EndOfMonth(month.AddDate(0, -1, 0))

		workSummary, err := s.repo.GetWorkSummaryForEmployeeInPeriod(ctx, database.GetWorkSummaryForEmployeeInPeriodParams{
			EmployeeID: fmt.Sprintf("%d", employee.ID),
			StartDate:  &startOfMonth,
			EndDate:    &endOfMonth,
		})
		if err != nil {
			s.logger.Error("could not get work summary for employee", "error", err)
			return []PayrollInput{}, err
		}

		payrollInput.WorkSummary = struct {
			Days    int64
			Hours   float64
			Minutes int64
		}(workSummary)

		sicknessSummary, err := s.repo.GetSicknessSummaryForEmployeeInPeriod(ctx, database.GetSicknessSummaryForEmployeeInPeriodParams{
			EmployeeID: fmt.Sprintf("%d", employee.ID),
			StartDate:  &startOfMonth,
			EndDate:    &endOfMonth,
		})
		if err != nil {
			s.logger.Error("could not get work summary for employee", "error", err)
			return []PayrollInput{}, err
		}

		payrollInput.SicknessSummary = struct {
			Days    int64
			Hours   float64
			Minutes int64
		}(sicknessSummary)

		worktimes, err := s.repo.ListWorkTimesForEmployeeInPeriod(ctx, database.ListWorkTimesForEmployeeInPeriodParams{
			EmployeeID: fmt.Sprintf("%d", employee.ID),
			StartDate:  &startOfMonth,
			EndDate:    &endOfMonth,
		})
		if err != nil {
			s.logger.Error("could not get worktimes for employee", "error", err)
			return []PayrollInput{}, nil
		}

		payrollInput.WorkTime = worktimes

		inputs = append(inputs, payrollInput)
	}

	return inputs, nil
}

func BeginningOfMonth(date time.Time) time.Time {
	return date.AddDate(0, 0, -date.Day()+1)
}

func EndOfMonth(date time.Time) time.Time {
	eom := date.AddDate(0, 1, -date.Day())
	return time.Date(
		eom.Year(),
		eom.Month(),
		eom.Day(),
		23, 59, 59, 999,
		eom.Location(),
	)
}
