package migration

import (
	"context"
	"encoding/csv"
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"

	"github.com/ultisuite/ulti-backend/internal/mail/hosted"
)

const (
	RosterStatusPending = "pending"
	RosterStatusInvited = "invited"
	RosterStatusClaimed = "claimed"
)

type RosterEntry struct {
	ID              string   `json:"id"`
	ProjectID       string   `json:"project_id"`
	Email           string   `json:"email"`
	DisplayName     string   `json:"display_name,omitempty"`
	AlternateEmails []string `json:"alternate_emails,omitempty"`
	Status          string   `json:"status"`
	InviteID        string   `json:"invite_id,omitempty"`
	CreatedAt       string   `json:"created_at"`
	UpdatedAt       string   `json:"updated_at"`
}

type RosterRowInput struct {
	Email           string
	DisplayName     string
	AlternateEmails []string
}

type RosterImportRowError struct {
	Row     int    `json:"row"`
	Email   string `json:"email,omitempty"`
	Message string `json:"message"`
}

type RosterImportResult struct {
	Created           int                    `json:"created"`
	SkippedDuplicates int                    `json:"skipped_duplicates"`
	Errors            []RosterImportRowError `json:"errors,omitempty"`
}

var rosterHeaderAliases = map[string]string{
	"email":             "email",
	"e-mail":            "email",
	"mail":              "email",
	"address":           "email",
	"display_name":      "display_name",
	"displayname":       "display_name",
	"name":              "display_name",
	"full_name":         "display_name",
	"alternate_emails":  "alternate_emails",
	"alternate_emails_": "alternate_emails",
	"alternates":        "alternate_emails",
	"alias":             "alternate_emails",
	"aliases":           "alternate_emails",
}

func ParseRosterCSV(r io.Reader) ([]RosterRowInput, error) {
	reader := csv.NewReader(r)
	reader.TrimLeadingSpace = true
	reader.FieldsPerRecord = -1

	var rows []RosterRowInput
	lineNum := 0
	emailCol := 0
	displayCol := -1
	alternateCol := -1
	headerResolved := false

	for {
		record, err := reader.Read()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("csv row %d: %w", lineNum+1, err)
		}
		lineNum++

		if len(record) == 0 {
			continue
		}
		for len(record) > 0 && strings.TrimSpace(record[len(record)-1]) == "" {
			record = record[:len(record)-1]
		}
		if len(record) == 0 {
			continue
		}

		if !headerResolved && looksLikeRosterHeader(record) {
			for i, col := range record {
				key := normalizeRosterHeader(col)
				switch key {
				case "email":
					emailCol = i
				case "display_name":
					displayCol = i
				case "alternate_emails":
					alternateCol = i
				}
			}
			headerResolved = true
			continue
		}
		headerResolved = true

		if emailCol >= len(record) {
			continue
		}
		email := normalizeInviteEmail(record[emailCol])
		if email == "" {
			continue
		}
		if !isEmailAddress(email) {
			return nil, fmt.Errorf("csv row %d: invalid email %q", lineNum, record[emailCol])
		}

		row := RosterRowInput{Email: email}
		if displayCol >= 0 && displayCol < len(record) {
			row.DisplayName = strings.TrimSpace(record[displayCol])
		}
		if alternateCol >= 0 && alternateCol < len(record) {
			row.AlternateEmails = parseAlternateEmailsField(record[alternateCol])
		} else if len(record) > 1 && displayCol < 0 && alternateCol < 0 {
			// email,display_name or email,alternates without header
			if len(record) > 1 {
				second := strings.TrimSpace(record[1])
				if strings.Contains(second, "@") {
					row.AlternateEmails = parseAlternateEmailsField(second)
				} else {
					row.DisplayName = second
				}
			}
			if len(record) > 2 {
				row.AlternateEmails = parseAlternateEmailsField(record[2])
			}
		}
		row.AlternateEmails = normalizeAlternateEmails(email, row.AlternateEmails)
		rows = append(rows, row)
	}

	return rows, nil
}

func looksLikeRosterHeader(record []string) bool {
	if len(record) == 0 {
		return false
	}
	first := normalizeRosterHeader(record[0])
	if first == "email" {
		return true
	}
	for _, col := range record {
		if normalizeRosterHeader(col) == "email" {
			return true
		}
	}
	return false
}

func normalizeRosterHeader(col string) string {
	key := strings.ToLower(strings.TrimSpace(col))
	key = strings.ReplaceAll(key, " ", "_")
	key = strings.ReplaceAll(key, "-", "_")
	if mapped, ok := rosterHeaderAliases[key]; ok {
		return mapped
	}
	return key
}

func parseAlternateEmailsField(raw string) []string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil
	}
	raw = strings.Trim(raw, "\"'")
	parts := strings.FieldsFunc(raw, func(r rune) bool {
		return r == ';' || r == '|' || r == ','
	})
	var out []string
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p != "" {
			out = append(out, p)
		}
	}
	return out
}

func (s *Service) ListRoster(ctx context.Context, projectID string) ([]RosterEntry, error) {
	rows, err := s.db.Query(ctx, `
		SELECT id::text, project_id::text, email, display_name, alternate_emails, status,
		       COALESCE(invite_id::text, ''), created_at::text, updated_at::text
		FROM migration_roster
		WHERE project_id = $1::uuid
		ORDER BY email
	`, projectID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []RosterEntry
	for rows.Next() {
		var row RosterEntry
		if err := rows.Scan(
			&row.ID, &row.ProjectID, &row.Email, &row.DisplayName, &row.AlternateEmails,
			&row.Status, &row.InviteID, &row.CreatedAt, &row.UpdatedAt,
		); err != nil {
			return nil, err
		}
		out = append(out, row)
	}
	return out, rows.Err()
}

func (s *Service) ImportRoster(ctx context.Context, projectID string, inputs []RosterRowInput) (RosterImportResult, error) {
	result := RosterImportResult{}
	for i, input := range inputs {
		rowNum := i + 1
		email := normalizeInviteEmail(input.Email)
		if email == "" || !isEmailAddress(email) {
			result.Errors = append(result.Errors, RosterImportRowError{
				Row: rowNum, Email: input.Email, Message: "invalid email",
			})
			continue
		}
		alternates := normalizeAlternateEmails(email, input.AlternateEmails)
		displayName := strings.TrimSpace(input.DisplayName)

		existingStatus, err := s.rosterStatusByEmail(ctx, projectID, email)
		if err != nil {
			return result, err
		}
		if existingStatus != "" {
			result.SkippedDuplicates++
			continue
		}

		tx, err := s.db.Begin(ctx)
		if err != nil {
			return result, err
		}

		var rosterID string
		err = tx.QueryRow(ctx, `
			INSERT INTO migration_roster (project_id, email, display_name, alternate_emails, status)
			VALUES ($1::uuid, $2, $3, $4, $5)
			RETURNING id::text
		`, projectID, email, displayName, alternates, RosterStatusPending).Scan(&rosterID)
		if err != nil {
			_ = tx.Rollback(ctx)
			if isUniqueViolation(err) {
				result.SkippedDuplicates++
				continue
			}
			result.Errors = append(result.Errors, RosterImportRowError{
				Row: rowNum, Email: email, Message: err.Error(),
			})
			continue
		}

		token, err := hosted.NewInviteToken()
		if err != nil {
			_ = tx.Rollback(ctx)
			return result, err
		}

		var inviteID string
		err = tx.QueryRow(ctx, `
			INSERT INTO migration_invites (project_id, email, token, alternate_emails)
			VALUES ($1::uuid, $2, $3, $4)
			RETURNING id::text
		`, projectID, email, token, alternates).Scan(&inviteID)
		if err != nil {
			_ = tx.Rollback(ctx)
			if isUniqueViolation(err) {
				result.SkippedDuplicates++
				continue
			}
			result.Errors = append(result.Errors, RosterImportRowError{
				Row: rowNum, Email: email, Message: err.Error(),
			})
			continue
		}

		_, err = tx.Exec(ctx, `
			UPDATE migration_roster
			SET status = $1, invite_id = $2::uuid, updated_at = NOW()
			WHERE id = $3::uuid
		`, RosterStatusInvited, inviteID, rosterID)
		if err != nil {
			_ = tx.Rollback(ctx)
			result.Errors = append(result.Errors, RosterImportRowError{
				Row: rowNum, Email: email, Message: err.Error(),
			})
			continue
		}

		if err := tx.Commit(ctx); err != nil {
			result.Errors = append(result.Errors, RosterImportRowError{
				Row: rowNum, Email: email, Message: err.Error(),
			})
			continue
		}
		result.Created++
	}
	return result, nil
}

func (s *Service) rosterStatusByEmail(ctx context.Context, projectID, email string) (string, error) {
	var status string
	err := s.db.QueryRow(ctx, `
		SELECT status FROM migration_roster
		WHERE project_id = $1::uuid AND email = $2
	`, projectID, email).Scan(&status)
	if errors.Is(err, pgx.ErrNoRows) {
		return "", nil
	}
	return status, err
}

func (s *Service) markRosterClaimed(ctx context.Context, tx pgx.Tx, projectID, inviteID, email string) error {
	_, err := tx.Exec(ctx, `
		UPDATE migration_roster
		SET status = $1, updated_at = NOW()
		WHERE project_id = $2::uuid
		  AND (invite_id = $3::uuid OR (invite_id IS NULL AND email = $4))
	`, RosterStatusClaimed, projectID, inviteID, email)
	return err
}

func isUniqueViolation(err error) bool {
	var pgErr *pgconn.PgError
	return errors.As(err, &pgErr) && pgErr.Code == "23505"
}
