package service

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"

	"github.com/calendarapi/internal/models"
	"github.com/calendarapi/internal/repository"
	"github.com/calendarapi/internal/utils"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"
)

type CalendarService struct {
	pool    *pgxpool.Pool
	queries *repository.Queries
	audit   *AuditService
	baseURL string
}

func NewCalendarService(pool *pgxpool.Pool, queries *repository.Queries, audit *AuditService, baseURL string) *CalendarService {
	return &CalendarService{pool: pool, queries: queries, audit: audit, baseURL: baseURL}
}

func generatePublicToken() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(b), nil
}

func generatePrivateToken() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	hash := sha256.Sum256(b)
	return hex.EncodeToString(hash[:]), nil
}

func (s *CalendarService) icalURL(token string) string {
	if token == "" {
		return ""
	}
	return fmt.Sprintf("%s/cal/%s/feed.ics", s.baseURL, token)
}

func (s *CalendarService) availabilityURL(token string) string {
	if token == "" {
		return ""
	}
	return fmt.Sprintf("%s/availability/%s", s.baseURL, token)
}

func (s *CalendarService) Create(ctx context.Context, userID uuid.UUID, name, color string) (*models.Calendar, error) {
	if err := utils.ValidateCalendarName(name); err != nil {
		return nil, err
	}
	if err := utils.ValidateColor(color); err != nil {
		return nil, err
	}
	if color == "" {
		color = "#3B82F6"
	}

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return nil, models.ErrInternal
	}
	defer tx.Rollback(ctx)
	qtx := s.queries.WithTx(tx)

	calID := uuid.New()
	cal, err := qtx.CreateCalendar(ctx, repository.CreateCalendarParams{
		ID:       utils.ToPgUUID(calID),
		OwnerID:  utils.ToPgUUID(userID),
		Name:     name,
		Color:    color,
		IsPublic: false,
	})
	if err != nil {
		return nil, models.ErrInternal
	}

	err = qtx.UpsertCalendarMember(ctx, repository.UpsertCalendarMemberParams{
		CalendarID: utils.ToPgUUID(calID),
		UserID:     utils.ToPgUUID(userID),
		Role:       "owner",
	})
	if err != nil {
		return nil, models.ErrInternal
	}

	privateToken, err := generatePrivateToken()
	if err != nil {
		return nil, models.ErrInternal
	}
	if err := qtx.SetCalendarPublicToken(ctx, repository.SetCalendarPublicTokenParams{
		ID:          utils.ToPgUUID(calID),
		PublicToken: pgtype.Text{String: privateToken, Valid: true},
	}); err != nil {
		return nil, models.ErrInternal
	}

	if err := tx.Commit(ctx); err != nil {
		return nil, models.ErrInternal
	}

	s.audit.Log(ctx, "calendar", calID, "CREATE_CALENDAR", userID)

	defRem := (*int)(nil)
	if cal.DefaultReminderMinutes.Valid {
		v := int(cal.DefaultReminderMinutes.Int32)
		defRem = &v
	}
	return &models.Calendar{
		ID:                     utils.FromPgUUID(cal.ID),
		Name:                   cal.Name,
		Color:                  cal.Color,
		IsPublic:               cal.IsPublic,
		CountForAvailability:   cal.CountForAvailability,
		DefaultReminderMinutes: defRem,
		SortOrder:              int(cal.SortOrder),
		ICalURL:                s.icalURL(privateToken),
		AvailabilityURL:       s.availabilityURL(privateToken),
		Role:                  "owner",
		CreatedAt:             utils.FromPgTimestamptz(cal.CreatedAt),
		UpdatedAt:             utils.FromPgTimestamptz(cal.UpdatedAt),
	}, nil
}

func (s *CalendarService) List(ctx context.Context, userID uuid.UUID) ([]models.Calendar, error) {
	rows, err := s.queries.ListCalendarsByUser(ctx, utils.ToPgUUID(userID))
	if err != nil {
		return nil, models.ErrInternal
	}

	calendars := make([]models.Calendar, len(rows))
	for i, r := range rows {
		defRem := (*int)(nil)
		if r.DefaultReminderMinutes.Valid {
			v := int(r.DefaultReminderMinutes.Int32)
			defRem = &v
		}
		calendars[i] = models.Calendar{
			ID:                     utils.FromPgUUID(r.ID),
			Name:                   r.Name,
			Color:                  r.Color,
			IsPublic:               r.IsPublic,
			CountForAvailability:   r.CountForAvailability,
			DefaultReminderMinutes: defRem,
			SortOrder:              int(r.SortOrder),
			Role:                   r.Role,
			CreatedAt:              utils.FromPgTimestamptz(r.CreatedAt),
			UpdatedAt:              utils.FromPgTimestamptz(r.UpdatedAt),
		}
	}

	// Populate ICalURL and AvailabilityURL (requires separate lookup since ListCalendarsByUser doesn't select public_token)
	for i := range calendars {
		cal, err := s.queries.GetCalendarByID(ctx, utils.ToPgUUID(calendars[i].ID))
		if err == nil && cal.PublicToken.Valid {
			calendars[i].ICalURL = s.icalURL(cal.PublicToken.String)
			calendars[i].AvailabilityURL = s.availabilityURL(cal.PublicToken.String)
		}
	}

	return calendars, nil
}

func (s *CalendarService) Get(ctx context.Context, userID uuid.UUID, calID uuid.UUID) (*models.Calendar, error) {
	role, err := s.getRole(ctx, calID, userID)
	if err != nil {
		return nil, err
	}

	cal, err := s.queries.GetCalendarByID(ctx, utils.ToPgUUID(calID))
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, models.ErrNotFound
		}
		return nil, models.ErrInternal
	}

	// Backfill private token for existing calendars that don't have one
	if !cal.IsPublic && !cal.PublicToken.Valid {
		privateToken, err := generatePrivateToken()
		if err == nil {
			_ = s.queries.SetCalendarPublicToken(ctx, repository.SetCalendarPublicTokenParams{
				ID:          utils.ToPgUUID(calID),
				PublicToken: pgtype.Text{String: privateToken, Valid: true},
			})
			cal.PublicToken = pgtype.Text{String: privateToken, Valid: true}
		}
	}

	defRem := (*int)(nil)
	if cal.DefaultReminderMinutes.Valid {
		v := int(cal.DefaultReminderMinutes.Int32)
		defRem = &v
	}
	return &models.Calendar{
		ID:                     utils.FromPgUUID(cal.ID),
		Name:                   cal.Name,
		Color:                  cal.Color,
		IsPublic:               cal.IsPublic,
		CountForAvailability:   cal.CountForAvailability,
		DefaultReminderMinutes: defRem,
		SortOrder:              int(cal.SortOrder),
		ICalURL:                s.icalURL(utils.FromPgTextValue(cal.PublicToken)),
		AvailabilityURL:       s.availabilityURL(utils.FromPgTextValue(cal.PublicToken)),
		Role:                  role,
		CreatedAt:             utils.FromPgTimestamptz(cal.CreatedAt),
		UpdatedAt:             utils.FromPgTimestamptz(cal.UpdatedAt),
	}, nil
}

func (s *CalendarService) Update(ctx context.Context, userID uuid.UUID, calID uuid.UUID, name, color *string, isPublic *bool, countForAvailability *bool, defaultReminderMinutes *int, sortOrder *int) (*models.Calendar, error) {
	role, err := s.getRole(ctx, calID, userID)
	if err != nil {
		return nil, err
	}

	if role != "owner" && role != "editor" {
		return nil, models.ErrForbidden
	}
	if isPublic != nil && role != "owner" {
		return nil, models.NewForbiddenError("only owner can change is_public")
	}

	if name != nil {
		if err := utils.ValidateCalendarName(*name); err != nil {
			return nil, err
		}
	}
	if color != nil {
		if err := utils.ValidateColor(*color); err != nil {
			return nil, err
		}
	}

	var pgPublic pgtype.Bool
	if isPublic != nil {
		pgPublic = pgtype.Bool{Bool: *isPublic, Valid: true}
	}
	var pgCountForAvail pgtype.Bool
	if countForAvailability != nil {
		pgCountForAvail = pgtype.Bool{Bool: *countForAvailability, Valid: true}
	}

	var pgDefRem pgtype.Int4
	if defaultReminderMinutes != nil {
		pgDefRem = pgtype.Int4{Int32: int32(*defaultReminderMinutes), Valid: true}
	}
	var pgSort pgtype.Int4
	if sortOrder != nil {
		pgSort = pgtype.Int4{Int32: int32(*sortOrder), Valid: true}
	}

	cal, err := s.queries.UpdateCalendar(ctx, repository.UpdateCalendarParams{
		ID:                     utils.ToPgUUID(calID),
		Name:                   utils.ToPgTextPtr(name),
		Color:                  utils.ToPgTextPtr(color),
		IsPublic:               pgPublic,
		CountForAvailability:   pgCountForAvail,
		DefaultReminderMinutes: pgDefRem,
		SortOrder:              pgSort,
	})
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, models.ErrNotFound
		}
		return nil, models.ErrInternal
	}

	if isPublic != nil {
		if *isPublic {
			// Switching to public: use base64url token
			token, err := generatePublicToken()
			if err != nil {
				return nil, models.ErrInternal
			}
			_ = s.queries.SetCalendarPublicToken(ctx, repository.SetCalendarPublicTokenParams{
				ID:          utils.ToPgUUID(calID),
				PublicToken: pgtype.Text{String: token, Valid: true},
			})
			cal.PublicToken = pgtype.Text{String: token, Valid: true}
		} else {
			// Switching to private: use SHA256 token (replace existing token)
			token, err := generatePrivateToken()
			if err != nil {
				return nil, models.ErrInternal
			}
			_ = s.queries.SetCalendarPublicToken(ctx, repository.SetCalendarPublicTokenParams{
				ID:          utils.ToPgUUID(calID),
				PublicToken: pgtype.Text{String: token, Valid: true},
			})
			cal.PublicToken = pgtype.Text{String: token, Valid: true}
		}
	} else if !cal.IsPublic && !cal.PublicToken.Valid {
		// Backfill: private calendar with no token (e.g. from before this feature)
		privateToken, err := generatePrivateToken()
		if err == nil {
			_ = s.queries.SetCalendarPublicToken(ctx, repository.SetCalendarPublicTokenParams{
				ID:          utils.ToPgUUID(calID),
				PublicToken: pgtype.Text{String: privateToken, Valid: true},
			})
			cal.PublicToken = pgtype.Text{String: privateToken, Valid: true}
		}
	}

	s.audit.Log(ctx, "calendar", calID, "UPDATE_CALENDAR", userID)

	defRem := (*int)(nil)
	if cal.DefaultReminderMinutes.Valid {
		v := int(cal.DefaultReminderMinutes.Int32)
		defRem = &v
	}
	return &models.Calendar{
		ID:                     utils.FromPgUUID(cal.ID),
		Name:                   cal.Name,
		Color:                  cal.Color,
		IsPublic:               cal.IsPublic,
		CountForAvailability:   cal.CountForAvailability,
		DefaultReminderMinutes: defRem,
		SortOrder:              int(cal.SortOrder),
		ICalURL:                s.icalURL(utils.FromPgTextValue(cal.PublicToken)),
		AvailabilityURL:       s.availabilityURL(utils.FromPgTextValue(cal.PublicToken)),
		Role:                  role,
		CreatedAt:             utils.FromPgTimestamptz(cal.CreatedAt),
		UpdatedAt:             utils.FromPgTimestamptz(cal.UpdatedAt),
	}, nil
}

func (s *CalendarService) Delete(ctx context.Context, userID uuid.UUID, calID uuid.UUID) error {
	role, err := s.getRole(ctx, calID, userID)
	if err != nil {
		return err
	}
	if role != "owner" {
		return models.ErrForbidden
	}

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return models.ErrInternal
	}
	defer tx.Rollback(ctx)
	qtx := s.queries.WithTx(tx)

	pgCalID := utils.ToPgUUID(calID)
	if err := qtx.SoftDeleteEventsByCalendar(ctx, pgCalID); err != nil {
		return models.ErrInternal
	}
	if err := qtx.SoftDeleteCalendar(ctx, pgCalID); err != nil {
		return models.ErrInternal
	}

	if err := tx.Commit(ctx); err != nil {
		return models.ErrInternal
	}

	s.audit.Log(ctx, "calendar", calID, "DELETE_CALENDAR", userID)
	return nil
}

func (s *CalendarService) Share(ctx context.Context, ownerID uuid.UUID, calID uuid.UUID, targetEmail, role string) error {
	ownerRole, err := s.getRole(ctx, calID, ownerID)
	if err != nil {
		return err
	}
	if ownerRole != "owner" {
		return models.ErrForbidden
	}
	if role != "editor" && role != "viewer" {
		return models.NewValidationError("role must be editor or viewer")
	}

	targetUser, err := s.queries.GetUserByEmail(ctx, utils.NormalizeEmail(targetEmail))
	if err != nil {
		if err == pgx.ErrNoRows {
			return models.NewNotFoundError("user not found")
		}
		return models.ErrInternal
	}

	targetID := utils.FromPgUUID(targetUser.ID)
	if targetID == ownerID {
		return models.NewValidationError("cannot share with yourself")
	}

	err = s.queries.UpsertCalendarMember(ctx, repository.UpsertCalendarMemberParams{
		CalendarID: utils.ToPgUUID(calID),
		UserID:     utils.ToPgUUID(targetID),
		Role:       role,
	})
	if err != nil {
		return models.ErrInternal
	}

	s.audit.Log(ctx, "calendar", calID, "SHARE_CALENDAR", ownerID)
	return nil
}

func (s *CalendarService) ListMembers(ctx context.Context, userID uuid.UUID, calID uuid.UUID) ([]models.CalendarMember, error) {
	if _, err := s.getRole(ctx, calID, userID); err != nil {
		return nil, err
	}

	rows, err := s.queries.ListCalendarMembers(ctx, utils.ToPgUUID(calID))
	if err != nil {
		return nil, models.ErrInternal
	}

	members := make([]models.CalendarMember, len(rows))
	for i, r := range rows {
		members[i] = models.CalendarMember{
			UserID: utils.FromPgUUID(r.UserID),
			Email:  r.Email,
			Role:   r.Role,
		}
	}
	return members, nil
}

func (s *CalendarService) RemoveMember(ctx context.Context, ownerID uuid.UUID, calID uuid.UUID, targetUserID uuid.UUID) error {
	role, err := s.getRole(ctx, calID, ownerID)
	if err != nil {
		return err
	}
	if role != "owner" {
		return models.ErrForbidden
	}

	targetRole, err := s.getRole(ctx, calID, targetUserID)
	if err != nil {
		return err
	}
	if targetRole == "owner" {
		return models.NewValidationError("cannot remove owner")
	}

	err = s.queries.DeleteCalendarMember(ctx, repository.DeleteCalendarMemberParams{
		CalendarID: utils.ToPgUUID(calID),
		UserID:     utils.ToPgUUID(targetUserID),
	})
	if err != nil {
		return models.ErrInternal
	}

	s.audit.Log(ctx, "calendar", calID, "REMOVE_MEMBER", ownerID)
	return nil
}

func (s *CalendarService) GetRole(ctx context.Context, calID uuid.UUID, userID uuid.UUID) (string, error) {
	return s.getRole(ctx, calID, userID)
}

func (s *CalendarService) getRole(ctx context.Context, calID uuid.UUID, userID uuid.UUID) (string, error) {
	role, err := s.queries.GetCalendarMemberRole(ctx, repository.GetCalendarMemberRoleParams{
		CalendarID: utils.ToPgUUID(calID),
		UserID:     utils.ToPgUUID(userID),
	})
	if err != nil {
		if err == pgx.ErrNoRows {
			return "", models.ErrNotFound
		}
		return "", models.ErrInternal
	}
	return role, nil
}
