package service

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"wucher/internal/domain/flight"
	fmreport "wucher/internal/domain/fm_report"
	"wucher/internal/shared/pkg/appctx"
	"wucher/internal/shared/pkg/uuidv7"
)

const reportCodeMaxAttempts = 5

type FMReportService struct {
	repo           fmreport.Repository
	historyBuilder fmreport.FleetHistoryBuilder
	codeResolver   fmreport.ReportCodeResolver
	usage          interface {
		RefreshAll(context.Context) error
	}
}

func NewFMReportService(repo fmreport.Repository) *FMReportService {
	return &FMReportService{repo: repo}
}

func (s *FMReportService) WithFleetHistoryBuilder(builder fmreport.FleetHistoryBuilder) *FMReportService {
	s.historyBuilder = builder
	return s
}

func (s *FMReportService) WithReportCodeResolver(resolver fmreport.ReportCodeResolver) *FMReportService {
	s.codeResolver = resolver
	return s
}

func (s *FMReportService) WithUsageRefresher(refresher interface {
	RefreshAll(context.Context) error
}) *FMReportService {
	s.usage = refresher
	return s
}

func (s *FMReportService) resolveHelicopterForReportCode(ctx context.Context, takeoverID []byte) ([]byte, string) {
	if s.codeResolver == nil || len(takeoverID) != 16 {
		return nil, ""
	}
	helicopterID, reportPrefix := s.codeResolver.ResolveHelicopterForReportCode(ctx, takeoverID)
	reportPrefix = strings.ToUpper(strings.TrimSpace(reportPrefix))
	if len(helicopterID) != 16 || reportPrefix == "" {
		return nil, ""
	}
	return helicopterID, reportPrefix
}

func (s *FMReportService) assignReportCode(ctx context.Context, row *fmreport.Report, helicopterID []byte, reportPrefix string) {
	if len(helicopterID) != 16 || reportPrefix == "" || len(row.ID) != 16 {
		return
	}
	for attempt := 0; attempt < reportCodeMaxAttempts; attempt++ {
		maxSeq, err := s.repo.MaxReportSeqByHelicopter(ctx, helicopterID)
		if err != nil {
			return
		}
		code := reportPrefix + "-" + fmt.Sprintf("%04d", maxSeq+1)
		if err := s.repo.SetReportCode(ctx, row.ID, code); err != nil {
			if errors.Is(err, fmreport.ErrDuplicateReportCode) {
				continue
			}
			return
		}
		row.ReportCode = &code
		return
	}
}

func (s *FMReportService) GetFleetHistory(ctx context.Context, reportID []byte) (*fmreport.FleetHistory, error) {
	if len(reportID) != 16 {
		return nil, fmt.Errorf("report id must be a valid UUID")
	}
	return s.repo.GetFleetHistoryByReportID(ctx, reportID)
}

func (s *FMReportService) UpsertForFlight(ctx context.Context, flightRow *flight.Flight, flightInspectionID []byte) (*fmreport.Report, error) {
	if flightRow == nil || len(flightRow.ID) != 16 {
		return nil, fmt.Errorf("flight is required")
	}
	if len(flightRow.TakeoverAcID) != 16 {
		return nil, fmt.Errorf("takeover id is required")
	}
	if len(flightInspectionID) != 16 {
		return nil, fmt.Errorf("flight inspection id is required")
	}

	row := &fmreport.Report{
		FlightID:           flightRow.ID,
		TakeoverID:         flightRow.TakeoverAcID,
		FlightInspectionID: flightInspectionID,
	}
	if userID := appctx.GetUserID(ctx); len(userID) == 16 {
		row.CreatedBy = userID
		row.UpdatedBy = userID
	}
	isNew := true
	if existing, err := s.repo.GetByFlightID(ctx, flightRow.ID); err == nil && existing != nil && len(existing.ID) == 16 {
		isNew = false
		if existing.CompletedAt != nil {
			return existing, nil
		}
		row.ID = existing.ID
		row.CreatedAt = existing.CreatedAt
		row.CreatedBy = existing.CreatedBy
		row.HelicopterID = existing.HelicopterID
		row.ReportCode = existing.ReportCode
		row.Engine1GpcN1 = existing.Engine1GpcN1
		row.Engine1PtcN2 = existing.Engine1PtcN2
		row.Engine2GpcN1 = existing.Engine2GpcN1
		row.Engine2PtcN2 = existing.Engine2PtcN2
	}

	var helicopterID []byte
	var reportPrefix string
	if isNew {
		helicopterID, reportPrefix = s.resolveHelicopterForReportCode(ctx, flightRow.TakeoverAcID)
		if len(helicopterID) == 16 {
			row.HelicopterID = helicopterID
		}
	}
	if err := s.repo.Upsert(ctx, row); err != nil {
		return nil, fmt.Errorf("failed to upsert fm report: %w", err)
	}
	if isNew {
		s.assignReportCode(ctx, row, helicopterID, reportPrefix)
	}

	// The fleet-status snapshot is frozen at COMPLETION (see Complete), not here — while the
	// report is open its fleet_status is shown live so fleet updates reflect.
	return row, nil
}

// snapshotFleetStatus freezes the current fleet status for a report (idempotent: no-op if a
// snapshot already exists for the report).
func (s *FMReportService) snapshotFleetStatus(ctx context.Context, row *fmreport.Report) {
	if s.historyBuilder == nil || len(row.ID) != 16 {
		return
	}
	if existing, _ := s.repo.GetFleetHistoryByReportID(ctx, row.ID); existing != nil {
		return
	}
	if history := s.historyBuilder.BuildFleetHistory(ctx, row.TakeoverID); history != nil {
		history.FMReportID = row.ID
		_ = s.repo.CreateFleetHistory(ctx, history)
	}
}

func (s *FMReportService) UpdateForFlight(ctx context.Context, flightID []byte, engine1GpcN1, engine1PtcN2, engine2GpcN1, engine2PtcN2 *string) (*fmreport.Report, error) {
	if len(flightID) != 16 {
		return nil, fmt.Errorf("flight id must be a valid UUID")
	}
	row, err := s.repo.GetByFlightID(ctx, flightID)
	if err != nil {
		return nil, err
	}
	if row == nil || len(row.ID) != 16 {
		return nil, fmreport.ErrNotFound
	}
	if row.CompletedAt != nil {
		return nil, fmreport.ErrCompleted
	}
	if engine1GpcN1 != nil {
		v := strings.TrimSpace(*engine1GpcN1)
		row.Engine1GpcN1 = &v
	}
	if engine1PtcN2 != nil {
		v := strings.TrimSpace(*engine1PtcN2)
		row.Engine1PtcN2 = &v
	}
	if engine2GpcN1 != nil {
		v := strings.TrimSpace(*engine2GpcN1)
		row.Engine2GpcN1 = &v
	}
	if engine2PtcN2 != nil {
		v := strings.TrimSpace(*engine2PtcN2)
		row.Engine2PtcN2 = &v
	}
	if userID := appctx.GetUserID(ctx); len(userID) == 16 {
		row.UpdatedBy = userID
	}
	if err := s.repo.Upsert(ctx, row); err != nil {
		return nil, fmt.Errorf("failed to update fm report: %w", err)
	}
	return row, nil
}

func (s *FMReportService) Complete(ctx context.Context, flightID []byte) (*fmreport.Report, error) {
	if len(flightID) != 16 {
		return nil, fmt.Errorf("flight id must be a valid UUID")
	}
	row, err := s.repo.GetByFlightID(ctx, flightID)
	if err != nil {
		return nil, err
	}
	if row == nil || len(row.ID) != 16 {
		return nil, fmreport.ErrNotFound
	}
	if row.CompletedAt != nil {
		if s.usage != nil {
			if err := s.usage.RefreshAll(ctx); err != nil {
				return nil, err
			}
		}
		return row, nil
	}

	now := time.Now().UTC()
	row.CompletedAt = &now
	if userID := appctx.GetUserID(ctx); len(userID) == 16 {
		row.CompletedBy = userID
		row.UpdatedBy = userID
	}
	if err := s.repo.Upsert(ctx, row); err != nil {
		return nil, fmt.Errorf("failed to complete fm report: %w", err)
	}
	// Freeze the fleet-status snapshot at the moment of completion.
	s.snapshotFleetStatus(ctx, row)
	if s.usage != nil {
		if err := s.usage.RefreshAll(ctx); err != nil {
			return nil, err
		}
	}
	return row, nil
}

func (s *FMReportService) GetByID(ctx context.Context, id []byte) (*fmreport.Report, error) {
	if len(id) != 16 {
		return nil, fmt.Errorf("id must be a valid UUID")
	}
	return s.repo.GetByID(ctx, id)
}

func (s *FMReportService) GetByFlightID(ctx context.Context, flightID []byte) (*fmreport.Report, error) {
	if len(flightID) != 16 {
		return nil, fmt.Errorf("flight id must be a valid UUID")
	}
	row, err := s.repo.GetByFlightID(ctx, flightID)
	if err != nil {
		return nil, err
	}
	return row, nil
}

func (s *FMReportService) List(ctx context.Context, filter fmreport.ListFilter, limit, offset int) ([]fmreport.Report, int64, error) {
	return s.repo.List(ctx, filter, limit, offset)
}

func (s *FMReportService) Delete(ctx context.Context, id []byte) error {
	if len(id) != 16 {
		return fmt.Errorf("id must be a valid UUID")
	}
	row, err := s.repo.GetByID(ctx, id)
	if err != nil {
		return err
	}
	if row == nil || len(row.ID) != 16 {
		return fmreport.ErrNotFound
	}
	var deletedBy []byte
	if userID := appctx.GetUserID(ctx); len(userID) == 16 {
		deletedBy = userID
	}
	return s.repo.Delete(ctx, id, deletedBy)
}

func (s *FMReportService) AttachmentReferenceType() string { return "fm_report" }

func (s *FMReportService) AttachmentReferenceID(row *fmreport.Report) string {
	if row == nil {
		return ""
	}
	id, err := uuidv7.BytesToString(row.ID)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(id)
}
