package query

import (
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/jinzhu/gorm"

	"github.com/photoprism/photoprism/internal/ai/face"
	"github.com/photoprism/photoprism/internal/entity"
	"github.com/photoprism/photoprism/internal/mutex"
	"github.com/photoprism/photoprism/pkg/clean"
)

// IDs represents a list of identifier strings.
type IDs []string

// FaceMap maps identification strings to face entities.
type FaceMap map[string]entity.Face

// ErrRetainedManualClusters indicates that candidate clusters could not be purged after merging
// because markers still reference them. Callers may treat this as a non-fatal warning.
var ErrRetainedManualClusters = errors.New("faces: retained manual clusters after merge")

// MergeMaxRetry limits how often the optimiser retries stubborn manual clusters (0 = unlimited).
var MergeMaxRetry = 1

func init() {
	if v := os.Getenv("PHOTOPRISM_FACE_MERGE_MAX_RETRY"); v != "" {
		if n, err := strconv.Atoi(v); err == nil {
			if n < 0 {
				n = 0
			}

			MergeMaxRetry = n
		}
	}
}

// FacesByID retrieves faces from the database and returns a map with the Face ID as key.
func FacesByID(knownOnly, unmatchedOnly, hidden, ignored bool) (FaceMap, IDs, error) {
	faces, err := Faces(knownOnly, unmatchedOnly, hidden, ignored)

	if err != nil {
		return nil, nil, err
	}

	faceIds := make(IDs, len(faces))
	faceMap := make(FaceMap, len(faces))

	for i, f := range faces {
		faceMap[f.ID] = f
		faceIds[i] = f.ID
	}

	return faceMap, faceIds, nil
}

// Faces returns all (known / unmatched) faces from the index.
func Faces(knownOnly, unmatchedOnly, hidden, ignored bool) (result entity.Faces, err error) {
	stmt := Db()

	if knownOnly {
		stmt = stmt.Where("subj_uid <> ''")
	}

	if unmatchedOnly {
		stmt = stmt.Where("matched_at IS NULL")
	}

	if !hidden {
		stmt = stmt.Where("face_hidden = ?", false)
	}

	if !ignored {
		stmt = stmt.Where("face_kind <= 1")
	}

	err = stmt.Order("subj_uid, samples DESC").Find(&result).Error

	return result, err
}

// ManuallyAddedFaces returns all manually added face clusters for the specified subj_uid, or all subjects if "".
func ManuallyAddedFaces(hidden, ignored bool, subjUid string) (result entity.Faces, err error) {
	stmt := Db().
		Where("face_hidden = ?", hidden).
		Where("face_src = ?", entity.SrcManual)

	if subjUid != "" {
		stmt = stmt.Where("subj_uid = ?", subjUid)
	} else {
		stmt = stmt.Where("subj_uid <> ''")
	}

	if MergeMaxRetry > 0 {
		stmt = stmt.Where("merge_retry < ?", MergeMaxRetry)
	}

	if !ignored {
		stmt = stmt.Where("face_kind <= 1")
	}

	err = stmt.Order("subj_uid, samples DESC, created_at ASC").Find(&result).Error

	return result, err
}

// MatchFaceMarkers matches markers with known faces.
func MatchFaceMarkers() (affected int64, err error) {
	faces, err := Faces(true, false, false, false)

	if err != nil {
		return affected, err
	}

	for _, f := range faces {
		if res := Db().Model(&entity.Marker{}).
			Where("marker_invalid = 0").
			Where("face_id = ?", f.ID).
			Where("subj_src = ?", entity.SrcAuto).
			Where("subj_uid <> ?", f.SubjUID).
			UpdateColumns(entity.Values{"subj_uid": f.SubjUID, "marker_review": false}); res.Error != nil {
			return affected, err
		} else if res.RowsAffected > 0 {
			affected += res.RowsAffected
		}
	}

	return affected, nil
}

// RemoveAnonymousFaceClusters removes anonymous faces from the index.
func RemoveAnonymousFaceClusters() (removed int, err error) {
	res := UnscopedDb().
		Delete(entity.Face{}, "subj_uid = '' AND face_src = ?", entity.SrcAuto)

	return int(res.RowsAffected), res.Error
}

// RemoveAutoFaceClusters removes automatically added face clusters from the index.
func RemoveAutoFaceClusters() (removed int, err error) {
	res := UnscopedDb().
		Delete(entity.Face{}, "face_src = ?", entity.SrcAuto)

	return int(res.RowsAffected), res.Error
}

// CountNewFaceMarkers counts the number of new face markers in the index.
func CountNewFaceMarkers(size, score int) (n int) {
	var f entity.Face

	if err := Db().Where("face_src = ?", entity.SrcAuto).
		Order("created_at DESC").Limit(1).Take(&f).Error; err != nil {
		log.Debugf("faces: found no existing clusters")
	}

	q := Db().Model(&entity.Markers{}).
		Where("marker_type = ?", entity.MarkerFace).
		Where("face_id = '' AND marker_invalid = 0 AND embeddings_json <> ''")

	if size > 0 {
		q = q.Where("size >= ?", size)
	}

	if score > 0 {
		q = q.Where("score >= ?", score)
	}

	if !f.CreatedAt.IsZero() {
		q = q.Where("created_at > ?", f.CreatedAt)
	}

	if err := q.Count(&n).Error; err != nil {
		log.Errorf("faces: %s (count new markers)", err)
	}

	return n
}

// PurgeOrphanFaces removes unused faces from the index.
func PurgeOrphanFaces(faceIds []string, ignored bool) (affected int, err error) {
	// Remove invalid face IDs in batches to be compatible with SQLite.
	batchSize := BatchSize()

	for i := 0; i < len(faceIds); i += batchSize {
		j := i + batchSize

		if j > len(faceIds) {
			j = len(faceIds)
		}

		// Next batch.
		ids := faceIds[i:j]

		// Remove invalid face IDs.
		stmt := Db().
			Where("id IN (?)", ids).
			Where("id NOT IN (SELECT face_id FROM ?)", gorm.Expr(entity.Marker{}.TableName()))

		if !ignored {
			stmt = stmt.Where("face_kind <= 1")
		}

		if result := stmt.Delete(&entity.Face{}); result.Error != nil {
			return affected, fmt.Errorf("faces: %s while purging orphan faces", result.Error)
		} else if result.RowsAffected > 0 {
			affected += int(result.RowsAffected)
		} else {
			// see https://github.com/photoprism/photoprism/issues/3124#issuecomment-2558299360
			log.Debugf("faces: no affected rows for purge in batch %d - %d", i, j)
			// affected += len(ids)
		}
	}

	return affected, nil
}

// MergeFaces returns a new face that replaces multiple others.
func MergeFaces(merge entity.Faces, ignored bool) (merged *entity.Face, err error) {
	if len(merge) < 2 {
		// Nothing to merge.
		return merged, fmt.Errorf("faces: two or more clusters required for merging")
	}

	subjUID := merge[0].SubjUID

	for i := 1; i < len(merge); i++ {
		if merge[i].SubjUID != subjUID {
			return merged, fmt.Errorf("faces: cannot merge clusters with conflicting subjects %s <> %s",
				clean.Log(subjUID), clean.Log(merge[i].SubjUID))
		}
	}

	// Find or create merged face cluster.
	if merged = entity.NewFace(merge[0].SubjUID, merge[0].FaceSrc, merge.Embeddings()); merged == nil {
		return merged, fmt.Errorf("faces: new cluster is nil for subject %s", clean.Log(subjUID))
	} else if merged = entity.FirstOrCreateFace(merged); merged == nil {
		return merged, fmt.Errorf("faces: failed to create new cluster for subject %s", clean.Log(subjUID))
	} else if err := merged.MatchMarkers(append(merge.IDs(), "")); err != nil {
		return merged, err
	}

	// PurgeOrphanFaces removes unused faces from the index.
	if removed, err := PurgeOrphanFaces(merge.IDs(), ignored); err != nil {
		return merged, err
	} else if removed > 0 {
		log.Debugf("faces: removed %d orphans of %d candidate for subject %s", removed, len(merge), clean.Log(subjUID))
	} else {
		note := fmt.Sprintf("retained markers after merge attempt on %s", time.Now().UTC().Format(time.RFC3339))

		for _, candidate := range merge {
			updates := entity.Values{
				"MergeRetry": gorm.Expr("merge_retry + 1"),
				"MergeNotes": note,
			}

			if err := Db().Model(&entity.Face{}).Where("id = ?", candidate.ID).Updates(updates).Error; err != nil {
				log.Warnf("faces: failed updating merge retry for %s (%s)", candidate.ID, err)
			} else {
				candidate.MergeRetry++
				candidate.MergeNotes = note
			}
		}

		return merged, fmt.Errorf("%w: kept %d candidate cluster(s) [%s] for subject %s because markers still reference them", ErrRetainedManualClusters, len(merge), clean.Log(strings.Join(merge.IDs(), ", ")), clean.Log(subjUID))
	}

	return merged, err
}

// ResetFaceMergeRetry clears merge retry metadata for all (or subject-specific) clusters.
func ResetFaceMergeRetry(subjUID string) (int, error) {
	stmt := Db().Model(&entity.Face{}).Where("merge_retry > 0")

	if subjUID != "" {
		stmt = stmt.Where("subj_uid = ?", subjUID)
	}

	res := stmt.UpdateColumns(entity.Values{"merge_retry": 0, "merge_notes": ""})

	if res.Error != nil {
		return 0, res.Error
	}

	return int(res.RowsAffected), nil
}

// ResolveFaceCollisions resolves collisions of different subject's faces.
func ResolveFaceCollisions() (conflicts, resolved int, err error) {
	faces, ids, err := FacesByID(true, false, false, false)

	if err != nil {
		return conflicts, resolved, err
	}

	// Remembers matched combinations.
	done := make(map[string]bool, len(ids)*len(ids))

	// Find face assignment collisions.
	for _, i := range ids {
		for _, j := range ids {
			var f1, f2 entity.Face

			if f, ok := faces[i]; ok {
				f1 = f
			} else {
				continue
			}

			if f, ok := faces[j]; ok {
				f2 = f
			} else {
				continue
			}

			var matchId string

			// Skip?
			if matchId = f1.MatchId(f2); matchId == "" || done[matchId] {
				continue
			}

			// Compare face 1 with face 2.
			if matched, dist := f1.Match(face.Embeddings{f2.Embedding()}); matched {
				if f1.SubjUID == f2.SubjUID {
					continue
				}

				conflicts++

				r := f1.SampleRadius + face.MatchDist

				log.Infof("faces: face %s has ambiguous subject at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)

				if f1.SubjUID != "" {
					log.Debugf("faces: face %s has %s subject %s (%s)", f1.ID, entity.SrcString(f1.FaceSrc), entity.SubjNames.Log(f1.SubjUID), f1.SubjUID)
				} else {
					log.Debugf("faces: face %s has unknown subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
				}

				if f2.SubjUID != "" {
					log.Debugf("faces: face %s has %s subject %s (%s)", f2.ID, entity.SrcString(f2.FaceSrc), entity.SubjNames.Log(f2.SubjUID), f2.SubjUID)
				} else {
					log.Debugf("faces: face %s has unknown subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
				}

				// Resolve.
				success, failed := f1.ResolveCollision(face.Embeddings{f2.Embedding()})

				// Failed?
				if failed != nil {
					log.Errorf("faces: conflict resolution for %s failed, face %s has collisions with other persons (%s)", entity.SubjNames.Log(f1.SubjUID), f1.ID, failed)
					continue
				}

				// Success?
				if success {
					log.Infof("faces: successful conflict resolution for %s, face %s had collisions with other persons", entity.SubjNames.Log(f1.SubjUID), f1.ID)
					resolved++
					faces, _, err = FacesByID(true, false, false, false)
					logErr("faces", "refresh", err)
				} else {
					log.Infof("faces: conflict resolution for %s not successful, face %s still has collisions with other persons", entity.SubjNames.Log(f1.SubjUID), f1.ID)
				}

				done[matchId] = true
			}
		}
	}

	return conflicts, resolved, nil
}

// RemovePeopleAndFaces permanently removes all people, faces, and face markers.
func RemovePeopleAndFaces() (err error) {
	mutex.Index.Lock()
	defer mutex.Index.Unlock()

	// Delete people.
	if err = UnscopedDb().Delete(entity.Subject{}, "subj_type = ?", entity.SubjPerson).Error; err != nil {
		return err
	}

	// Delete all faces.
	if err = UnscopedDb().Delete(entity.Face{}).Error; err != nil {
		return err
	}

	// Delete face markers.
	if err = UnscopedDb().Delete(entity.Marker{}, "marker_type = ?", entity.MarkerFace).Error; err != nil {
		return err
	}

	// Reset face counters.
	if err = UnscopedDb().Model(entity.Photo{}).
		UpdateColumn("photo_faces", 0).Error; err != nil {
		return err
	}

	// Reset people label.
	if label, labelErr := LabelBySlug("people"); labelErr != nil {
		return labelErr
	} else if labelErr = UnscopedDb().
		Delete(entity.PhotoLabel{}, "label_id = ?", label.ID).Error; labelErr != nil {
		return labelErr
	} else if labelErr = label.Update("PhotoCount", 0); labelErr != nil {
		return labelErr
	}

	// Reset portrait label.
	if label, labelErr := LabelBySlug("portrait"); labelErr != nil {
		return labelErr
	} else if labelErr = UnscopedDb().
		Delete(entity.PhotoLabel{}, "label_id = ?", label.ID).Error; labelErr != nil {
		return labelErr
	} else if labelErr = label.Update("PhotoCount", 0); labelErr != nil {
		return labelErr
	}

	return nil
}
