package contacts

import (
	"context"
	"encoding/json"
	"fmt"
	"regexp"
	"sort"
	"strings"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/ultisuite/ulti-backend/internal/api/paginate"
	"github.com/ultisuite/ulti-backend/internal/api/query"
	"github.com/ultisuite/ulti-backend/internal/auth"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
)

type Service struct {
	nc *nextcloud.Client
	db *pgxpool.Pool
}

func NewService(nc *nextcloud.Client, db *pgxpool.Pool) *Service {
	return &Service{nc: nc, db: db}
}

func (s *Service) EnsureNextcloudUser(ctx context.Context, claims *auth.Claims) (string, error) {
	if s.nc == nil {
		return "", fmt.Errorf("nextcloud unavailable")
	}
	return s.nc.EnsurePrincipal(ctx, claims.Email, claims.Sub, claims.Name)
}

// ReprovisionPrincipal drops cached CardDAV credentials and provisions fresh app password.
func (s *Service) ReprovisionPrincipal(ctx context.Context, claims *auth.Claims) (string, error) {
	if s.nc == nil {
		return "", fmt.Errorf("nextcloud unavailable")
	}
	userID := nextcloud.UserIDFromClaims(claims.Email, claims.Sub)
	if err := s.nc.InvalidatePrincipalCredentials(ctx, userID); err != nil {
		return "", err
	}
	return s.nc.EnsurePrincipal(ctx, claims.Email, claims.Sub, claims.Name)
}

func bookPath(userID, bookID string) string {
	return nextcloud.AddressBookPath(userID, bookID)
}

func (s *Service) ListAddressBooks(ctx context.Context, userID string) ([]nextcloud.AddressBook, error) {
	return s.nc.ListAddressBooks(ctx, userID)
}

type ContactsList struct {
	Contacts   []nextcloud.Contact  `json:"contacts"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListContacts(ctx context.Context, userID, bookID string, params query.ListParams) (ContactsList, error) {
	contacts, err := s.nc.ListContacts(ctx, userID, bookPath(userID, bookID))
	if err != nil {
		return ContactsList{}, err
	}
	filtered := filterContacts(contacts, params.Q)
	page, total := paginate.Slice(filtered, params.Offset(), params.Limit())
	return ContactsList{
		Contacts:   page,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) SearchContacts(ctx context.Context, userID, bookID, q string, params query.ListParams) (ContactsList, error) {
	searchQ := strings.TrimSpace(q)
	if searchQ == "" {
		searchQ = strings.TrimSpace(params.Q)
	}
	if searchQ == "" {
		var zero int64
		return ContactsList{
			Contacts:   []nextcloud.Contact{},
			Pagination: params.Meta(&zero),
		}, nil
	}

	contacts, err := s.nc.ListContacts(ctx, userID, bookPath(userID, bookID))
	if err != nil {
		return ContactsList{}, err
	}
	ranked := rankContactsByQuery(contacts, searchQ)
	page, total := paginate.Slice(ranked, params.Offset(), params.Limit())
	return ContactsList{
		Contacts:   page,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) CreateContact(ctx context.Context, userID, bookID string, contact *nextcloud.Contact) (*nextcloud.Contact, error) {
	return s.nc.CreateContact(ctx, userID, bookPath(userID, bookID), contact)
}

func (s *Service) GetContact(ctx context.Context, userID, contactPath string) (*nextcloud.Contact, error) {
	return s.nc.GetContact(ctx, userID, contactPath)
}

func (s *Service) UpdateContact(ctx context.Context, userID, contactPath, ifMatch string, contact *nextcloud.Contact) (string, error) {
	return s.nc.UpdateContact(ctx, userID, contactPath, ifMatch, contact)
}

func (s *Service) DeleteContact(ctx context.Context, userID, contactPath string) error {
	return s.nc.DeleteContact(ctx, userID, contactPath)
}

func (s *Service) SyncContacts(ctx context.Context, userID, bookID, syncToken string) (nextcloud.ContactSyncResult, error) {
	return s.nc.SyncContacts(ctx, userID, bookPath(userID, bookID), syncToken)
}

type MergeDuplicatesRequest struct {
	DryRun bool `json:"dry_run"`
}

type DuplicateGroup struct {
	Key        string              `json:"key"`
	KeyType    string              `json:"key_type"`
	Primary    nextcloud.Contact   `json:"primary"`
	Duplicates []nextcloud.Contact `json:"duplicates"`
}

type MergeDuplicatesResult struct {
	DryRun      bool             `json:"dry_run"`
	TotalGroups int              `json:"total_groups"`
	MergedCount int              `json:"merged_count"`
	Groups      []DuplicateGroup `json:"groups"`
}

func (s *Service) MergeDuplicates(ctx context.Context, userID, bookID string, req MergeDuplicatesRequest) (MergeDuplicatesResult, error) {
	contacts, err := s.nc.ListContacts(ctx, userID, bookPath(userID, bookID))
	if err != nil {
		return MergeDuplicatesResult{}, err
	}

	groupsByKey := map[string][]nextcloud.Contact{}
	groupType := map[string]string{}
	for _, c := range contacts {
		key, keyType := dedupeKey(c)
		if key == "" {
			continue
		}
		groupsByKey[key] = append(groupsByKey[key], c)
		groupType[key] = keyType
	}

	result := MergeDuplicatesResult{
		DryRun: req.DryRun,
		Groups: make([]DuplicateGroup, 0),
	}

	for key, group := range groupsByKey {
		if len(group) < 2 {
			continue
		}
		sort.SliceStable(group, func(i, j int) bool {
			return contactScore(group[i]) > contactScore(group[j])
		})
		primary := group[0]
		duplicates := group[1:]
		merged := primary
		for _, d := range duplicates {
			merged = mergeContactFields(merged, d)
		}

		result.Groups = append(result.Groups, DuplicateGroup{
			Key:        key,
			KeyType:    groupType[key],
			Primary:    merged,
			Duplicates: duplicates,
		})

		if req.DryRun {
			continue
		}

		updatePath := strings.TrimSpace(primary.Path)
		if updatePath == "" && strings.TrimSpace(primary.UID) != "" {
			updatePath = fmt.Sprintf("%s%s.vcf", bookPath(userID, bookID), strings.TrimSpace(primary.UID))
		}
		if updatePath != "" {
			if _, err := s.nc.UpdateContact(ctx, userID, updatePath, primary.ETag, &merged); err != nil {
				return MergeDuplicatesResult{}, err
			}
		}

		for _, d := range duplicates {
			if strings.TrimSpace(d.Path) == "" {
				continue
			}
			if err := s.nc.DeleteContact(ctx, userID, d.Path); err != nil {
				return MergeDuplicatesResult{}, err
			}
			result.MergedCount++
		}
	}

	sort.SliceStable(result.Groups, func(i, j int) bool {
		return result.Groups[i].Key < result.Groups[j].Key
	})
	result.TotalGroups = len(result.Groups)
	return result, nil
}

type MailInteraction struct {
	ID        string    `json:"id"`
	AccountID string    `json:"account_id"`
	Subject   string    `json:"subject"`
	Snippet   string    `json:"snippet"`
	Date      time.Time `json:"date"`
}

type MeetingInteraction struct {
	CalendarID string `json:"calendar_id"`
	UID        string `json:"uid"`
	Summary    string `json:"summary"`
	Start      string `json:"start"`
	End        string `json:"end"`
	Location   string `json:"location,omitempty"`
	Path       string `json:"path,omitempty"`
}

type FileInteraction struct {
	ID          string    `json:"id"`
	MessageID   string    `json:"message_id"`
	Subject     string    `json:"subject"`
	Filename    string    `json:"filename"`
	ContentType string    `json:"content_type"`
	Size        int64     `json:"size"`
	Date        time.Time `json:"date"`
}

type ContactInteractions struct {
	Email    string               `json:"email"`
	Mail     []MailInteraction    `json:"mail"`
	Meetings []MeetingInteraction `json:"meetings"`
	Files    []FileInteraction    `json:"files"`
}

func (s *Service) ContactInteractionsByPath(ctx context.Context, userID, contactPath string, limit int) (ContactInteractions, error) {
	contact, err := s.nc.GetContact(ctx, userID, contactPath)
	if err != nil {
		return ContactInteractions{}, err
	}
	email := strings.TrimSpace(contact.Email)
	if email == "" {
		return ContactInteractions{}, ErrContactEmailMissing
	}
	return s.ContactInteractionsByEmail(ctx, userID, email, limit)
}

func (s *Service) ContactInteractionsByEmail(ctx context.Context, userID, email string, limit int) (ContactInteractions, error) {
	out := ContactInteractions{
		Email:    email,
		Mail:     []MailInteraction{},
		Meetings: []MeetingInteraction{},
		Files:    []FileInteraction{},
	}
	if limit <= 0 {
		limit = 20
	}
	if s.db != nil {
		mailRows, err := s.db.Query(ctx, `
			SELECT m.id, m.account_id, m.subject, m.snippet, m.date
			FROM messages m
			JOIN mail_accounts ma ON m.account_id = ma.id
			WHERE ma.user_id = (SELECT id FROM users WHERE external_id = $1)
			  AND (
			    EXISTS (SELECT 1 FROM jsonb_array_elements(m.from_addr) a WHERE lower(coalesce(a->>'address', '')) = lower($2))
			    OR EXISTS (SELECT 1 FROM jsonb_array_elements(m.to_addrs) a WHERE lower(coalesce(a->>'address', '')) = lower($2))
			    OR EXISTS (SELECT 1 FROM jsonb_array_elements(m.cc_addrs) a WHERE lower(coalesce(a->>'address', '')) = lower($2))
			  )
			ORDER BY m.date DESC
			LIMIT $3
		`, userID, email, limit)
		if err != nil {
			return ContactInteractions{}, err
		}
		for mailRows.Next() {
			var item MailInteraction
			if err := mailRows.Scan(&item.ID, &item.AccountID, &item.Subject, &item.Snippet, &item.Date); err != nil {
				mailRows.Close()
				return ContactInteractions{}, err
			}
			out.Mail = append(out.Mail, item)
		}
		if err := mailRows.Err(); err != nil {
			mailRows.Close()
			return ContactInteractions{}, err
		}
		mailRows.Close()

		fileRows, err := s.db.Query(ctx, `
			SELECT a.id, m.id, m.subject, a.filename, a.content_type, a.size, m.date
			FROM attachments a
			JOIN messages m ON a.message_id = m.id
			JOIN mail_accounts ma ON m.account_id = ma.id
			WHERE ma.user_id = (SELECT id FROM users WHERE external_id = $1)
			  AND (
			    EXISTS (SELECT 1 FROM jsonb_array_elements(m.from_addr) f WHERE lower(coalesce(f->>'address', '')) = lower($2))
			    OR EXISTS (SELECT 1 FROM jsonb_array_elements(m.to_addrs) t WHERE lower(coalesce(t->>'address', '')) = lower($2))
			    OR EXISTS (SELECT 1 FROM jsonb_array_elements(m.cc_addrs) c WHERE lower(coalesce(c->>'address', '')) = lower($2))
			  )
			ORDER BY m.date DESC
			LIMIT $3
		`, userID, email, limit)
		if err != nil {
			return ContactInteractions{}, err
		}
		for fileRows.Next() {
			var item FileInteraction
			if err := fileRows.Scan(&item.ID, &item.MessageID, &item.Subject, &item.Filename, &item.ContentType, &item.Size, &item.Date); err != nil {
				fileRows.Close()
				return ContactInteractions{}, err
			}
			out.Files = append(out.Files, item)
		}
		if err := fileRows.Err(); err != nil {
			fileRows.Close()
			return ContactInteractions{}, err
		}
		fileRows.Close()
	}

	if s.nc != nil {
		cals, err := s.nc.ListCalendars(ctx, userID)
		if err != nil {
			return ContactInteractions{}, err
		}
		now := time.Now().UTC()
		from := now.AddDate(0, -6, 0)
		to := now.AddDate(1, 0, 0)
		for _, cal := range cals {
			events, err := s.nc.ListEvents(ctx, userID, "/remote.php/dav/calendars/"+userID+"/"+cal.ID+"/", from, to)
			if err != nil {
				return ContactInteractions{}, err
			}
			for _, ev := range events {
				if !eventMatchesEmail(ev, email) {
					continue
				}
				out.Meetings = append(out.Meetings, MeetingInteraction{
					CalendarID: cal.ID,
					UID:        ev.UID,
					Summary:    ev.Summary,
					Start:      ev.Start,
					End:        ev.End,
					Location:   ev.Location,
					Path:       ev.Path,
				})
			}
		}
		sort.SliceStable(out.Meetings, func(i, j int) bool {
			return out.Meetings[i].Start > out.Meetings[j].Start
		})
		if len(out.Meetings) > limit {
			out.Meetings = out.Meetings[:limit]
		}
	}

	return out, nil
}

var onlyDigits = regexp.MustCompile(`\D+`)

func dedupeKey(c nextcloud.Contact) (string, string) {
	email := strings.ToLower(strings.TrimSpace(c.Email))
	if email != "" {
		return "email:" + email, "email"
	}
	phone := onlyDigits.ReplaceAllString(strings.TrimSpace(c.Phone), "")
	if phone != "" {
		return "phone:" + phone, "phone"
	}
	name := strings.ToLower(strings.TrimSpace(c.FullName))
	org := strings.ToLower(strings.TrimSpace(c.Org))
	if name != "" && org != "" {
		return "name_org:" + name + "|" + org, "name_org"
	}
	return "", ""
}

func contactScore(c nextcloud.Contact) int {
	score := 0
	if strings.TrimSpace(c.FullName) != "" {
		score += 3
	}
	if strings.TrimSpace(c.Email) != "" {
		score += 4
	}
	if strings.TrimSpace(c.Phone) != "" {
		score += 2
	}
	if strings.TrimSpace(c.Org) != "" {
		score += 1
	}
	if strings.TrimSpace(c.Path) != "" {
		score += 1
	}
	return score
}

func mergeContactFields(primary, candidate nextcloud.Contact) nextcloud.Contact {
	out := primary
	if strings.TrimSpace(out.FullName) == "" {
		out.FullName = candidate.FullName
	}
	if strings.TrimSpace(out.Email) == "" {
		out.Email = candidate.Email
	}
	if strings.TrimSpace(out.Phone) == "" {
		out.Phone = candidate.Phone
	}
	if strings.TrimSpace(out.Org) == "" {
		out.Org = candidate.Org
	}
	if strings.TrimSpace(out.UID) == "" {
		out.UID = candidate.UID
	}
	return out
}

func eventMatchesEmail(event nextcloud.Event, email string) bool {
	if strings.EqualFold(strings.TrimSpace(event.Organizer), strings.TrimSpace(email)) {
		return true
	}
	for _, attendee := range event.Attendees {
		if strings.EqualFold(strings.TrimSpace(attendee.Email), strings.TrimSpace(email)) {
			return true
		}
	}
	return false
}

var ErrContactEmailMissing = fmt.Errorf("contact email is required for interaction enrichment")

type mailAddress struct {
	Name    string `json:"name"`
	Address string `json:"address"`
}

func extractAddresses(raw []byte) []string {
	if len(raw) == 0 {
		return nil
	}
	var addrs []mailAddress
	if err := json.Unmarshal(raw, &addrs); err != nil {
		return nil
	}
	out := make([]string, 0, len(addrs))
	for _, addr := range addrs {
		if strings.TrimSpace(addr.Address) != "" {
			out = append(out, strings.ToLower(strings.TrimSpace(addr.Address)))
		}
	}
	return out
}

func filterContacts(contacts []nextcloud.Contact, q string) []nextcloud.Contact {
	return rankContactsByQuery(contacts, q)
}
