package models

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/timetracker/backend/internal/db"
	"github.com/timetracker/backend/internal/types"
	"gorm.io/gorm"
)

// TimeEntry represents a time entry in the system
type TimeEntry struct {
	EntityBase
	UserID      types.ULID `gorm:"column:user_id;type:bytea;not null;index"`
	ProjectID   types.ULID `gorm:"column:project_id;type:bytea;not null;index"`
	ActivityID  types.ULID `gorm:"column:activity_id;type:bytea;not null;index"`
	Start       time.Time  `gorm:"column:start;not null"`
	End         time.Time  `gorm:"column:end;not null"`
	Description string     `gorm:"column:description"`
	Billable    int        `gorm:"column:billable"` // Percentage (0-100)

	// Relationships for Eager Loading
	User     *User     `gorm:"foreignKey:UserID"`
	Project  *Project  `gorm:"foreignKey:ProjectID"`
	Activity *Activity `gorm:"foreignKey:ActivityID"`
}

// TableName specifies the table name for GORM
func (TimeEntry) TableName() string {
	return "time_entries"
}

// TimeEntryCreate contains the fields for creating a new time entry
type TimeEntryCreate struct {
	UserID      types.ULID
	ProjectID   types.ULID
	ActivityID  types.ULID
	Start       time.Time
	End         time.Time
	Description string
	Billable    int // Percentage (0-100)
}

// TimeEntryUpdate contains the updatable fields of a time entry
type TimeEntryUpdate struct {
	ID          types.ULID  `gorm:"-"` // Exclude from updates
	UserID      *types.ULID `gorm:"column:user_id"`
	ProjectID   *types.ULID `gorm:"column:project_id"`
	ActivityID  *types.ULID `gorm:"column:activity_id"`
	Start       *time.Time  `gorm:"column:start"`
	End         *time.Time  `gorm:"column:end"`
	Description *string     `gorm:"column:description"`
	Billable    *int        `gorm:"column:billable"`
}

// Validate checks if the Create struct contains valid data
func (tc *TimeEntryCreate) Validate() error {
	// Check for empty IDs
	if tc.UserID.Compare(types.ULID{}) == 0 {
		return errors.New("userID cannot be empty")
	}
	if tc.ProjectID.Compare(types.ULID{}) == 0 {
		return errors.New("projectID cannot be empty")
	}
	if tc.ActivityID.Compare(types.ULID{}) == 0 {
		return errors.New("activityID cannot be empty")
	}

	// Time checks
	if tc.Start.IsZero() {
		return errors.New("start time cannot be empty")
	}
	if tc.End.IsZero() {
		return errors.New("end time cannot be empty")
	}
	if tc.End.Before(tc.Start) {
		return errors.New("end time cannot be before start time")
	}

	// Billable percentage check
	if tc.Billable < 0 || tc.Billable > 100 {
		return errors.New("billable must be between 0 and 100")
	}

	return nil
}

// Validate checks if the Update struct contains valid data
func (tu *TimeEntryUpdate) Validate() error {
	// Billable percentage check
	if tu.Billable != nil && (*tu.Billable < 0 || *tu.Billable > 100) {
		return errors.New("billable must be between 0 and 100")
	}

	// Time checks
	if tu.Start != nil && tu.End != nil && tu.End.Before(*tu.Start) {
		return errors.New("end time cannot be before start time")
	}

	return nil
}

// GetTimeEntryByID finds a time entry by its ID
func GetTimeEntryByID(ctx context.Context, id types.ULID) (*TimeEntry, error) {
	var timeEntry TimeEntry
	result := db.GetEngine(ctx).Where("id = ?", id).First(&timeEntry)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, result.Error
	}
	return &timeEntry, nil
}

// GetTimeEntryWithRelations loads a time entry with all associated data
func GetTimeEntryWithRelations(ctx context.Context, id types.ULID) (*TimeEntry, error) {
	var timeEntry TimeEntry
	result := db.GetEngine(ctx).
		Preload("User").
		Preload("Project").
		Preload("Project.Customer"). // Nested relationship
		Preload("Activity").
		Where("id = ?", id).
		First(&timeEntry)

	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, result.Error
	}
	return &timeEntry, nil
}

// GetAllTimeEntries returns all time entries
func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) {
	var timeEntries []TimeEntry
	result := db.GetEngine(ctx).Find(&timeEntries)
	if result.Error != nil {
		return nil, result.Error
	}
	return timeEntries, nil
}

// GetTimeEntriesByUserID returns all time entries of a user
func GetTimeEntriesByUserID(ctx context.Context, userID types.ULID) ([]TimeEntry, error) {
	var timeEntries []TimeEntry
	result := db.GetEngine(ctx).Where("user_id = ?", userID).Find(&timeEntries)
	if result.Error != nil {
		return nil, result.Error
	}
	return timeEntries, nil
}

// GetTimeEntriesByProjectID returns all time entries of a project
func GetTimeEntriesByProjectID(ctx context.Context, projectID types.ULID) ([]TimeEntry, error) {
	var timeEntries []TimeEntry
	result := db.GetEngine(ctx).Where("project_id = ?", projectID).Find(&timeEntries)
	if result.Error != nil {
		return nil, result.Error
	}
	return timeEntries, nil
}

// GetTimeEntriesByDateRange returns all time entries within a time range
func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]TimeEntry, error) {
	var timeEntries []TimeEntry
	// Search for overlaps in the time range
	result := db.GetEngine(ctx).
		Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)",
			start, end, start, end).
		Find(&timeEntries)

	if result.Error != nil {
		return nil, result.Error
	}
	return timeEntries, nil
}

// SumBillableHoursByProject calculates the billable hours per project
func SumBillableHoursByProject(ctx context.Context, projectID types.ULID) (float64, error) {
	type Result struct {
		TotalHours float64
	}

	var result Result

	// SQL calculation of weighted hours
	err := db.GetEngine(ctx).Raw(`
        SELECT SUM(
            EXTRACT(EPOCH FROM (end - start)) / 3600 * (billable / 100.0)
        ) as total_hours 
        FROM time_entries 
        WHERE project_id = ?
    `, projectID).Scan(&result).Error

	if err != nil {
		return 0, err
	}

	return result.TotalHours, nil
}

// CreateTimeEntry creates a new time entry with validation
func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, error) {
	// Validation
	if err := create.Validate(); err != nil {
		return nil, fmt.Errorf("validation error: %w", err)
	}

	// Start a transaction
	var timeEntry *TimeEntry

	err := db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
		// Check references
		if err := validateReferences(tx, create.UserID, create.ProjectID, create.ActivityID); err != nil {
			return err
		}

		// Create time entry
		newTimeEntry := TimeEntry{
			UserID:      create.UserID,
			ProjectID:   create.ProjectID,
			ActivityID:  create.ActivityID,
			Start:       create.Start,
			End:         create.End,
			Description: create.Description,
			Billable:    create.Billable,
		}

		if err := tx.Create(&newTimeEntry).Error; err != nil {
			return fmt.Errorf("error creating the time entry: %w", err)
		}

		timeEntry = &newTimeEntry
		return nil
	})

	if err != nil {
		return nil, err
	}

	return timeEntry, nil
}

// validateReferences checks if all referenced entities exist
func validateReferences(tx *gorm.DB, userID, projectID, activityID types.ULID) error {
	// Check user
	var userCount int64
	if err := tx.Model(&User{}).Where("id = ?", userID).Count(&userCount).Error; err != nil {
		return fmt.Errorf("error checking the user: %w", err)
	}
	if userCount == 0 {
		return errors.New("the specified user does not exist")
	}

	// Check project
	var projectCount int64
	if err := tx.Model(&Project{}).Where("id = ?", projectID).Count(&projectCount).Error; err != nil {
		return fmt.Errorf("error checking the project: %w", err)
	}
	if projectCount == 0 {
		return errors.New("the specified project does not exist")
	}

	// Check activity
	var activityCount int64
	if err := tx.Model(&Activity{}).Where("id = ?", activityID).Count(&activityCount).Error; err != nil {
		return fmt.Errorf("error checking the activity: %w", err)
	}
	if activityCount == 0 {
		return errors.New("the specified activity does not exist")
	}

	return nil
}

// UpdateTimeEntry updates an existing time entry with validation
func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, error) {
	// Validation
	if err := update.Validate(); err != nil {
		return nil, fmt.Errorf("validation error: %w", err)
	}

	// Find time entry
	timeEntry, err := GetTimeEntryByID(ctx, update.ID)
	if err != nil {
		return nil, err
	}
	if timeEntry == nil {
		return nil, errors.New("time entry not found")
	}

	// Start a transaction for the update
	err = db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
		// Check references if they are updated
		if update.UserID != nil || update.ProjectID != nil || update.ActivityID != nil {
			// Use current values if not updated
			userID := timeEntry.UserID
			if update.UserID != nil {
				userID = *update.UserID
			}

			projectID := timeEntry.ProjectID
			if update.ProjectID != nil {
				projectID = *update.ProjectID
			}

			activityID := timeEntry.ActivityID
			if update.ActivityID != nil {
				activityID = *update.ActivityID
			}

			if err := validateReferences(tx, userID, projectID, activityID); err != nil {
				return err
			}
		}

		// Check time consistency
		start := timeEntry.Start
		if update.Start != nil {
			start = *update.Start
		}

		end := timeEntry.End
		if update.End != nil {
			end = *update.End
		}

		if end.Before(start) {
			return errors.New("end time cannot be before start time")
		}

		// Use generic update
		if err := UpdateModel(ctx, timeEntry, update); err != nil {
			return fmt.Errorf("error updating the time entry: %w", err)
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	// Load updated data from the database
	return GetTimeEntryByID(ctx, update.ID)
}

// DeleteTimeEntry deletes a time entry by its ID
func DeleteTimeEntry(ctx context.Context, id types.ULID) error {
	result := db.GetEngine(ctx).Delete(&TimeEntry{}, id)
	if result.Error != nil {
		return fmt.Errorf("error deleting the time entry: %w", result.Error)
	}
	return nil
}
