package entity

import (
	"fmt"
	"strings"
	"time"

	"github.com/dustin/go-humanize/english"

	"github.com/photoprism/photoprism/internal/ai/classify"
	"github.com/photoprism/photoprism/internal/service/maps"
	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/geo"
	"github.com/photoprism/photoprism/pkg/time/tz"
	"github.com/photoprism/photoprism/pkg/txt"
)

// SetCoordinates updates latitude/longitude while honoring source priority and keeping altitude in sync.
func (m *Photo) SetCoordinates(lat, lng, altitude float64, source string) {
	m.SetAltitude(altitude, source)

	if lat == 0.0 && lng == 0.0 {
		return
	}

	if SrcPriority[source] < SrcPriority[m.PlaceSrc] && m.HasLatLng() {
		return
	}

	m.PhotoLat = lat
	m.PhotoLng = lng
	m.PlaceSrc = source
}

// SetAltitude updates the stored altitude when the new value passes cleanup and the source outranks the current one.
func (m *Photo) SetAltitude(altitude float64, source string) {
	a := clean.Altitude(altitude)

	if a == 0 && source != SrcManual {
		return
	}

	if SrcPriority[source] < SrcPriority[m.PlaceSrc] {
		return
	}

	m.PhotoAltitude = a
}

// UnknownLocation tests if the photo has an unknown location.
func (m *Photo) UnknownLocation() bool {
	return m.CellID == "" || m.CellID == UnknownLocation.ID || m.NoLatLng()
}

// SetPosition records an estimated position, randomizing estimates slightly and refreshing location metadata.
func (m *Photo) SetPosition(pos geo.Position, source string, force bool) {
	if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force {
		return
	} else if pos.Lat == 0 && pos.Lng == 0 {
		return
	}

	if m.CellID != UnknownID && pos.InRange(m.PhotoLat, m.PhotoLng, geo.Meter*50) {
		log.Debugf("photo: %s keeps position %f, %f", m.String(), m.PhotoLat, m.PhotoLng)
	} else {
		if pos.Estimate {
			pos.Randomize(geo.Meter * 5)
		}

		m.PhotoLat = pos.Lat
		m.PhotoLng = pos.Lng
		m.PlaceSrc = source
		m.CellAccuracy = pos.Accuracy
		m.SetAltitude(pos.Altitude, source)

		log.Debugf("photo: %s %s", m.String(), pos.String())

		m.UpdateLocation()

		if m.Place == nil {
			log.Warnf("photo: failed to update position of %s", m)
		} else {
			log.Debugf("photo: approximate place of %s is %s (id %s)", m, clean.Log(m.Place.Label()), m.PlaceID)
		}
	}
}

// AdoptPlace copies place metadata from another photo when the source priority allows it.
func (m *Photo) AdoptPlace(other *Photo, source string, force bool) {
	if other == nil {
		return
	} else if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force {
		return
	} else if other.Place == nil {
		return
	} else if other.Place.Unknown() {
		return
	}

	// Remove existing location labels if place changes.
	if other.Place.ID != m.PlaceID {
		m.RemoveLocationLabels()
	}

	m.RemoveLocation(source, force)

	m.Place = other.Place
	m.PlaceID = other.PlaceID
	m.PhotoCountry = other.PhotoCountry
	m.PlaceSrc = source

	m.UpdateTimeZone(other.TimeZone)

	log.Debugf("photo: %s now located at %s (id %s)", m.String(), clean.Log(m.Place.Label()), m.PlaceID)
}

// RemoveLocation clears the current location metadata when the caller is authorized via source priority.
func (m *Photo) RemoveLocation(source string, force bool) {
	if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force {
		return
	}

	// Reset latitude and longitude.
	m.PhotoLat = 0
	m.PhotoLng = 0

	// Reset cell reference.
	m.Cell = &UnknownLocation
	m.CellID = UnknownLocation.ID
	m.CellAccuracy = 0

	// Reset country code.
	m.PhotoCountry = UnknownCountry.ID

	// Reset place reference.
	m.Place = &UnknownPlace
	m.PlaceID = UnknownPlace.ID

	// Reset place source.
	m.PlaceSrc = SrcAuto
}

// RemoveLocationLabels removes labels created from prior location data to keep annotations consistent.
func (m *Photo) RemoveLocationLabels() {
	if len(m.Labels) == 0 {
		res := Db().Delete(PhotoLabel{}, "photo_id = ? AND label_src = ?", m.ID, SrcLocation)

		if res.Error != nil {
			Log("photo", "remove location labels", res.Error)
		} else if res.RowsAffected > 0 {
			log.Infof("photo: removed %s from %s",
				english.Plural(int(res.RowsAffected), "location label", "location labels"), m)
		}

		return
	}

	labels := make([]PhotoLabel, 0, len(m.Labels))

	for _, l := range m.Labels {
		if l.LabelSrc != SrcLocation {
			labels = append(labels, l)
			continue
		}

		Log("photo", "remove location label", l.Delete())
	}

	removed := len(m.Labels) - len(labels)

	if removed > 0 {
		log.Infof("photo: removed %s from %s",
			english.Plural(removed, "location label", "location labels"), m)
		m.Labels = labels
	}
}

// HasLocation tests if the photo has a known location.
func (m *Photo) HasLocation() bool {
	return !m.UnknownLocation()
}

// TrustedLocation tests if the photo has a known location from a trusted source.
func (m *Photo) TrustedLocation() bool {
	return m.HasLocation() && SrcPriority[m.PlaceSrc] > SrcPriority[SrcEstimate]
}

// LocationLoaded tests if the photo has a known location that is currently loaded.
func (m *Photo) LocationLoaded() bool {
	if m.Cell == nil {
		return false
	}

	if m.Cell.Place == nil {
		return false
	}

	return !m.Cell.Unknown() && m.Cell.ID == m.CellID
}

// LoadLocation fetches the full Cell record (including Place) from the database when needed.
func (m *Photo) LoadLocation() error {
	if m.LocationLoaded() {
		return nil
	}

	if m.UnknownLocation() {
		return fmt.Errorf("photo: unknown location (%s)", m)
	}

	var location Cell

	err := Db().Preload("Place").First(&location, "id = ?", m.CellID).Error

	if err != nil {
		return err
	}

	if location.Place == nil {
		location.Place = &UnknownPlace
		location.PlaceID = UnknownPlace.ID
	}

	m.Cell = &location

	return nil
}

// PlaceLoaded checks if the photo has a known place that is currently loaded.
func (m *Photo) PlaceLoaded() bool {
	if m.Place == nil {
		return false
	}

	return !m.Place.Unknown() && m.Place.ID == m.PlaceID
}

// LoadPlace fetches the associated Place from the database when needed.
func (m *Photo) LoadPlace() error {
	if m.PlaceLoaded() {
		return nil
	}

	if m.UnknownPlace() {
		return fmt.Errorf("photo: unknown place (%s)", m)
	}

	var place Place

	err := Db().First(&place, "id = ?", m.PlaceID).Error

	if err != nil {
		return err
	}

	m.Place = &place

	return nil
}

// Position returns the coordinates as geo.Position.
func (m *Photo) Position() geo.Position {
	return geo.Position{Name: m.String(), Time: m.TakenAt.UTC(),
		Lat: m.PhotoLat, Lng: m.PhotoLng, Altitude: float64(m.PhotoAltitude)}
}

// HasLatLng checks if the photo has a latitude and longitude.
func (m *Photo) HasLatLng() bool {
	return m.PhotoLat != 0.0 || m.PhotoLng != 0.0
}

// NoLatLng checks if latitude and longitude are missing.
func (m *Photo) NoLatLng() bool {
	return !m.HasLatLng()
}

// UnknownPlace checks if the photo has an unknown place.
func (m *Photo) UnknownPlace() bool {
	return m.PlaceID == "" || m.PlaceID == UnknownPlace.ID
}

// HasPlace checks if the photo has a known place.
func (m *Photo) HasPlace() bool {
	return !m.UnknownPlace()
}

// HasCountry checks if the photo has a known country.
func (m *Photo) HasCountry() bool {
	return !m.UnknownCountry()
}

// UnknownCountry checks if the photo has an unknown country.
func (m *Photo) UnknownCountry() bool {
	return m.CountryCode() == UnknownCountry.ID
}

// LocationTimeZone uses the GPS location to determine the time zone of the photo,
// and returns "Local" if no location is set or no time zone is found.
func (m *Photo) LocationTimeZone() string {
	return tz.Position(m.PhotoLat, m.PhotoLng)
}

// CountryName returns the photo country name.
func (m *Photo) CountryName() string {
	if name, ok := maps.CountryNames[m.CountryCode()]; ok {
		return name
	}

	return UnknownCountry.CountryName
}

// CountryCode returns the photo country code.
func (m *Photo) CountryCode() string {
	if len(m.PhotoCountry) != 2 {
		m.PhotoCountry = UnknownCountry.ID
	}

	return m.PhotoCountry
}

// GetTakenAt returns UTC time for TakenAtLocal.
func (m *Photo) GetTakenAt() time.Time {
	location := tz.Find(m.TimeZone)

	if location == nil {
		return m.TakenAt
	}

	if takenAt, err := time.ParseInLocation("2006-01-02T15:04:05", m.TakenAtLocal.Format("2006-01-02T15:04:05"), location); err != nil {
		return m.TakenAt
	} else {
		return takenAt.UTC()
	}
}

// GetTakenAtLocal returns local time for TakenAt.
func (m *Photo) GetTakenAtLocal() time.Time {
	location := tz.Find(m.TimeZone)

	if location == tz.TimeLocal {
		return m.TakenAtLocal
	}

	if takenAtLocal, err := time.ParseInLocation("2006-01-02T15:04:05", m.TakenAt.In(location).Format("2006-01-02T15:04:05"), time.UTC); err != nil {
		return m.TakenAtLocal
	} else {
		return takenAtLocal.UTC()
	}
}

// UpdateLocation resolves cells, places, time zones, and labels from the current coordinates.
func (m *Photo) UpdateLocation() (keywords []string, labels classify.Labels) {
	if m.HasLatLng() {
		var loc = NewCell(m.PhotoLat, m.PhotoLng)

		if loc.Unknown() {
			// Empty or unknown S2 cell id... should not happen, unless coordinates are invalid.
			log.Warnf("photo: unknown cell id for lat %f, lng %f (uid %s)", m.PhotoLat, m.PhotoLng, m.PhotoUID)
		} else if err := loc.Find(GeoApi); err != nil {
			log.Errorf("photo: %s (find location)", err)
		} else if loc.Place == nil {
			log.Warnf("photo: failed fetching geo data (uid %s, cell %s)", m.PhotoUID, loc.ID)
		} else if loc.ID != UnknownLocation.ID {
			changed := m.CellID != loc.ID

			if changed {
				log.Debugf("photo: changing location of %s from %s to %s", m.String(), m.CellID, loc.ID)
				m.RemoveLocationLabels()
			}

			m.Cell = loc
			m.CellID = loc.ID
			m.Place = loc.Place
			m.PlaceID = loc.PlaceID
			m.PhotoCountry = loc.CountryCode()

			// Set time zone based on location.
			if changed {
				m.UpdateTimeZone(m.LocationTimeZone())
			}

			FirstOrCreateCountry(NewCountry(loc.CountryCode(), loc.CountryName()))

			locCategory := loc.Category()
			keywords = append(keywords, loc.Keywords()...)

			// Append category from reverse location lookup
			if locCategory != "" {
				labels = append(labels, classify.LocationLabel(locCategory, 0))
			}

			return keywords, labels
		}
	}

	keywords = []string{}
	labels = classify.Labels{}

	if m.UnknownLocation() {
		m.Cell = &UnknownLocation
		m.CellID = UnknownLocation.ID

		// Remove place estimate if better data is available.
		if SrcPriority[m.PlaceSrc] > SrcPriority[SrcEstimate] {
			m.Place = &UnknownPlace
			m.PlaceID = UnknownPlace.ID
		}
	} else if err := m.LoadLocation(); err == nil {
		m.Place = m.Cell.Place
		m.PlaceID = m.Cell.PlaceID
	} else {
		log.Warnf("photo: location %s not found in %s", m.CellID, m.PhotoName)
	}

	if m.UnknownPlace() {
		m.Place = &UnknownPlace
		m.PlaceID = UnknownPlace.ID
	} else if err := m.LoadPlace(); err == nil {
		m.PhotoCountry = m.Place.CountryCode()
	} else {
		log.Warnf("photo: place %s not found in %s", m.PlaceID, m.PhotoName)
	}

	if m.UnknownCountry() && m.CellID == UnknownID && m.PlaceID == UnknownID {
		m.EstimateCountry()
	}

	if m.HasCountry() {
		FirstOrCreateCountry(NewCountry(m.CountryCode(), m.CountryName()))
	}

	return keywords, labels
}

// SaveLocation applies UpdateLocation, synchronizes derived labels/keywords, and persists the photo.
func (m *Photo) SaveLocation() error {
	locKeywords, labels := m.UpdateLocation()

	m.AddLabels(labels)

	w := txt.UniqueWords(txt.Words(m.GetDetails().Keywords))
	w = append(w, locKeywords...)

	m.GetDetails().Keywords = strings.Join(txt.UniqueWords(w), ", ")

	if err := m.UpdateKeywordLabels(); err != nil {
		log.Errorf("photo: %s %s while updating keyword labels", m.String(), err)
	}

	if err := m.GenerateTitle(m.ClassifyLabels()); err != nil {
		log.Info(err)
	}

	if err := m.IndexKeywords(); err != nil {
		log.Errorf("photo: %s %s while indexing keywords", m.String(), err)
	}

	return m.Save()
}
