package services

import (
	"encoding/json"
	"sort"

	"gorm.io/gorm"

	"github.com/treytartt/honeydue-api/internal/apperrors"
	"github.com/treytartt/honeydue-api/internal/dto/responses"
	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/repositories"
)

// SuggestionService handles residence-aware task template suggestions
type SuggestionService struct {
	db            *gorm.DB
	residenceRepo *repositories.ResidenceRepository
}

// NewSuggestionService creates a new suggestion service
func NewSuggestionService(db *gorm.DB, residenceRepo *repositories.ResidenceRepository) *SuggestionService {
	return &SuggestionService{
		db:            db,
		residenceRepo: residenceRepo,
	}
}

// templateConditions represents the parsed conditions JSON from a task template.
// Every field is optional; a template with no conditions is "universal" and
// receives a small base score. See scoreTemplate for how each field is used.
type templateConditions struct {
	HeatingType     *string `json:"heating_type,omitempty"`
	CoolingType     *string `json:"cooling_type,omitempty"`
	WaterHeaterType *string `json:"water_heater_type,omitempty"`
	RoofType        *string `json:"roof_type,omitempty"`
	ExteriorType    *string `json:"exterior_type,omitempty"`
	FlooringPrimary *string `json:"flooring_primary,omitempty"`
	LandscapingType *string `json:"landscaping_type,omitempty"`
	HasPool         *bool   `json:"has_pool,omitempty"`
	HasSprinkler    *bool   `json:"has_sprinkler_system,omitempty"`
	HasSeptic       *bool   `json:"has_septic,omitempty"`
	HasFireplace    *bool   `json:"has_fireplace,omitempty"`
	HasGarage       *bool   `json:"has_garage,omitempty"`
	HasBasement     *bool   `json:"has_basement,omitempty"`
	HasAttic        *bool   `json:"has_attic,omitempty"`
	PropertyType    *string `json:"property_type,omitempty"`
	// ClimateRegionID replaces the old task_tasktemplate_regions join table.
	// Tag a template with the IECC zone ID it's relevant to (e.g. "Winterize
	// Sprinkler" → zone 5/6). Residence.PostalCode is mapped to a region at
	// scoring time via ZipToState + GetClimateRegionIDByState.
	ClimateRegionID *uint `json:"climate_region_id,omitempty"`
}

// isEmpty returns true if no conditions are set
func (c *templateConditions) isEmpty() bool {
	return c.HeatingType == nil && c.CoolingType == nil && c.WaterHeaterType == nil &&
		c.RoofType == nil && c.ExteriorType == nil && c.FlooringPrimary == nil &&
		c.LandscapingType == nil && c.HasPool == nil && c.HasSprinkler == nil &&
		c.HasSeptic == nil && c.HasFireplace == nil && c.HasGarage == nil &&
		c.HasBasement == nil && c.HasAttic == nil &&
		c.PropertyType == nil && c.ClimateRegionID == nil
}

const (
	maxSuggestions     = 30
	baseUniversalScore = 0.3
	stringMatchBonus   = 0.25
	boolMatchBonus     = 0.3
	// climateRegionBonus is deliberately higher than stringMatchBonus —
	// climate zone is coarse but high-signal (one bit for a whole region of
	// templates like "Hurricane Prep" or "Winterize Sprinkler").
	climateRegionBonus = 0.35
	propertyTypeBonus  = 0.15
	totalProfileFields = 15 // 14 home-profile fields + ZIP/region
)

// GetSuggestions returns task template suggestions scored against a residence's profile
func (s *SuggestionService) GetSuggestions(residenceID uint, userID uint) (*responses.TaskSuggestionsResponse, error) {
	// Check access
	hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID)
	if err != nil {
		return nil, apperrors.Internal(err)
	}
	if !hasAccess {
		return nil, apperrors.Forbidden("error.residence_access_denied")
	}

	// Load residence
	residence, err := s.residenceRepo.FindByID(residenceID)
	if err != nil {
		return nil, apperrors.Internal(err)
	}

	// Load all active templates
	var templates []models.TaskTemplate
	if err := s.db.
		Preload("Category").
		Preload("Frequency").
		Where("is_active = ?", true).
		Find(&templates).Error; err != nil {
		return nil, apperrors.Internal(err)
	}

	// Score each template
	suggestions := make([]responses.TaskSuggestionResponse, 0, len(templates))
	for i := range templates {
		score, reasons, include := s.scoreTemplate(&templates[i], residence)
		if !include {
			continue
		}
		suggestions = append(suggestions, responses.TaskSuggestionResponse{
			Template:       responses.NewTaskTemplateResponse(&templates[i]),
			RelevanceScore: score,
			MatchReasons:   reasons,
		})
	}

	// Sort by score descending
	sort.Slice(suggestions, func(i, j int) bool {
		return suggestions[i].RelevanceScore > suggestions[j].RelevanceScore
	})

	// Cap at maxSuggestions
	if len(suggestions) > maxSuggestions {
		suggestions = suggestions[:maxSuggestions]
	}

	completeness := CalculateProfileCompleteness(residence)

	return &responses.TaskSuggestionsResponse{
		Suggestions:         suggestions,
		TotalCount:          len(suggestions),
		ProfileCompleteness: completeness,
	}, nil
}

// scoreTemplate scores a template against a residence profile.
// Returns (score, matchReasons, shouldInclude).
func (s *SuggestionService) scoreTemplate(tmpl *models.TaskTemplate, residence *models.Residence) (float64, []string, bool) {
	cond := &templateConditions{}

	// Parse conditions JSON
	if len(tmpl.Conditions) > 0 && string(tmpl.Conditions) != "{}" && string(tmpl.Conditions) != "null" {
		if err := json.Unmarshal(tmpl.Conditions, cond); err != nil {
			// Malformed conditions - treat as universal
			cond = &templateConditions{}
		}
	}

	// Universal template (no conditions): base score
	if cond.isEmpty() {
		return baseUniversalScore, []string{"universal"}, true
	}

	score := 0.0
	reasons := make([]string, 0)
	conditionCount := 0

	// String field matches
	if cond.HeatingType != nil {
		conditionCount++
		if residence.HeatingType == nil {
			// Field not set - ignore
		} else if *residence.HeatingType == *cond.HeatingType {
			score += stringMatchBonus
			reasons = append(reasons, "heating_type:"+*cond.HeatingType)
		} else {
			// Mismatch - don't exclude, just don't reward
		}
	}

	if cond.CoolingType != nil {
		conditionCount++
		if residence.CoolingType == nil {
			// ignore
		} else if *residence.CoolingType == *cond.CoolingType {
			score += stringMatchBonus
			reasons = append(reasons, "cooling_type:"+*cond.CoolingType)
		}
	}

	if cond.WaterHeaterType != nil {
		conditionCount++
		if residence.WaterHeaterType == nil {
			// ignore
		} else if *residence.WaterHeaterType == *cond.WaterHeaterType {
			score += stringMatchBonus
			reasons = append(reasons, "water_heater_type:"+*cond.WaterHeaterType)
		}
	}

	if cond.RoofType != nil {
		conditionCount++
		if residence.RoofType == nil {
			// ignore
		} else if *residence.RoofType == *cond.RoofType {
			score += stringMatchBonus
			reasons = append(reasons, "roof_type:"+*cond.RoofType)
		}
	}

	if cond.ExteriorType != nil {
		conditionCount++
		if residence.ExteriorType == nil {
			// ignore
		} else if *residence.ExteriorType == *cond.ExteriorType {
			score += stringMatchBonus
			reasons = append(reasons, "exterior_type:"+*cond.ExteriorType)
		}
	}

	if cond.FlooringPrimary != nil {
		conditionCount++
		if residence.FlooringPrimary == nil {
			// ignore
		} else if *residence.FlooringPrimary == *cond.FlooringPrimary {
			score += stringMatchBonus
			reasons = append(reasons, "flooring_primary:"+*cond.FlooringPrimary)
		}
	}

	if cond.LandscapingType != nil {
		conditionCount++
		if residence.LandscapingType == nil {
			// ignore
		} else if *residence.LandscapingType == *cond.LandscapingType {
			score += stringMatchBonus
			reasons = append(reasons, "landscaping_type:"+*cond.LandscapingType)
		}
	}

	// Bool field matches - exclude if requires true but residence has false
	if cond.HasPool != nil {
		conditionCount++
		if *cond.HasPool && !residence.HasPool {
			return 0, nil, false // EXCLUDE
		}
		if *cond.HasPool && residence.HasPool {
			score += boolMatchBonus
			reasons = append(reasons, "has_pool")
		}
	}

	if cond.HasSprinkler != nil {
		conditionCount++
		if *cond.HasSprinkler && !residence.HasSprinklerSystem {
			return 0, nil, false
		}
		if *cond.HasSprinkler && residence.HasSprinklerSystem {
			score += boolMatchBonus
			reasons = append(reasons, "has_sprinkler_system")
		}
	}

	if cond.HasSeptic != nil {
		conditionCount++
		if *cond.HasSeptic && !residence.HasSeptic {
			return 0, nil, false
		}
		if *cond.HasSeptic && residence.HasSeptic {
			score += boolMatchBonus
			reasons = append(reasons, "has_septic")
		}
	}

	if cond.HasFireplace != nil {
		conditionCount++
		if *cond.HasFireplace && !residence.HasFireplace {
			return 0, nil, false
		}
		if *cond.HasFireplace && residence.HasFireplace {
			score += boolMatchBonus
			reasons = append(reasons, "has_fireplace")
		}
	}

	if cond.HasGarage != nil {
		conditionCount++
		if *cond.HasGarage && !residence.HasGarage {
			return 0, nil, false
		}
		if *cond.HasGarage && residence.HasGarage {
			score += boolMatchBonus
			reasons = append(reasons, "has_garage")
		}
	}

	if cond.HasBasement != nil {
		conditionCount++
		if *cond.HasBasement && !residence.HasBasement {
			return 0, nil, false
		}
		if *cond.HasBasement && residence.HasBasement {
			score += boolMatchBonus
			reasons = append(reasons, "has_basement")
		}
	}

	if cond.HasAttic != nil {
		conditionCount++
		if *cond.HasAttic && !residence.HasAttic {
			return 0, nil, false
		}
		if *cond.HasAttic && residence.HasAttic {
			score += boolMatchBonus
			reasons = append(reasons, "has_attic")
		}
	}

	// Property type match
	if cond.PropertyType != nil {
		conditionCount++
		if residence.PropertyType != nil && residence.PropertyType.Name == *cond.PropertyType {
			score += propertyTypeBonus
			reasons = append(reasons, "property_type:"+*cond.PropertyType)
		}
	}

	// Climate region match. We resolve the residence's ZIP to a region ID on
	// demand; a missing/invalid ZIP is treated the same as a nil home-profile
	// field — no penalty, no exclusion.
	if cond.ClimateRegionID != nil {
		conditionCount++
		if residenceRegionID := resolveResidenceRegionID(residence); residenceRegionID != 0 && residenceRegionID == *cond.ClimateRegionID {
			score += climateRegionBonus
			reasons = append(reasons, "climate_region")
		}
	}

	// Cap at 1.0
	if score > 1.0 {
		score = 1.0
	}

	// If template has conditions but no matches and no reasons, still include with low score
	if conditionCount > 0 && len(reasons) == 0 {
		return baseUniversalScore * 0.5, []string{"partial_profile"}, true
	}

	return score, reasons, true
}

// CalculateProfileCompleteness returns how many of the 14 home profile fields are filled
func CalculateProfileCompleteness(residence *models.Residence) float64 {
	filled := 0

	if residence.HeatingType != nil {
		filled++
	}
	if residence.CoolingType != nil {
		filled++
	}
	if residence.WaterHeaterType != nil {
		filled++
	}
	if residence.RoofType != nil {
		filled++
	}
	if residence.HasPool {
		filled++
	}
	if residence.HasSprinklerSystem {
		filled++
	}
	if residence.HasSeptic {
		filled++
	}
	if residence.HasFireplace {
		filled++
	}
	if residence.HasGarage {
		filled++
	}
	if residence.HasBasement {
		filled++
	}
	if residence.HasAttic {
		filled++
	}
	if residence.ExteriorType != nil {
		filled++
	}
	if residence.FlooringPrimary != nil {
		filled++
	}
	if residence.LandscapingType != nil {
		filled++
	}
	// PostalCode is the 15th field — counts toward completeness when we can
	// map it to a region. An invalid / unknown ZIP doesn't count.
	if resolveResidenceRegionIDByZip(residence.PostalCode) != 0 {
		filled++
	}

	return float64(filled) / float64(totalProfileFields)
}

// resolveResidenceRegionID returns the IECC climate zone ID for a residence
// based on its PostalCode, or 0 if the ZIP can't be mapped. Helper lives here
// (not in region_lookup.go) because it couples the Residence model to the
// suggestion service's notion of region resolution.
func resolveResidenceRegionID(residence *models.Residence) uint {
	return resolveResidenceRegionIDByZip(residence.PostalCode)
}

func resolveResidenceRegionIDByZip(zip string) uint {
	if zip == "" {
		return 0
	}
	state := ZipToState(zip)
	if state == "" {
		return 0
	}
	return GetClimateRegionIDByState(state)
}
