package service

import (
	"context"
	"fmt"
	"log/slog"
	"strings"
	"time"

	complaint "wucher/internal/domain/complaint"
	filemanager "wucher/internal/domain/file_manager"
	fleethistory "wucher/internal/domain/fleet_history"
	fleetstatus "wucher/internal/domain/fleet_status"
	"wucher/internal/domain/helicopter"
	helicopterusage "wucher/internal/domain/helicopter_usage"
	"wucher/internal/shared/pkg/nextdue"
	"wucher/internal/shared/pkg/uuidv7"
	shareddto "wucher/internal/transport/http/dto/shared"
)

type fleetStatusAttachments interface {
	CreateAttachment(ctx context.Context, params filemanager.CreateAttachmentParams) (*filemanager.Attachment, error)
	DeleteAttachment(ctx context.Context, id []byte) error
}

type fleetStatusHelicopters interface {
	GetByID(ctx context.Context, id []byte) (*helicopter.Helicopter, error)
	Update(ctx context.Context, h *helicopter.Helicopter) error
}

type fleetStatusComplaints interface {
	ListByHelicopter(ctx context.Context, helicopterID []byte, limit, offset int) ([]complaint.Complaint, int64, error)
	ResolveUserNames(ctx context.Context, complaints []complaint.Complaint) (map[string]string, error)
}

type fleetStatusUsage interface {
	GetByHelicopterID(ctx context.Context, helicopterID []byte) (*helicopterusage.HelicopterUsage, error)
	ReviseManualTotals(ctx context.Context, helicopterID []byte, in helicopterusage.ManualSummaryInput, actor []byte) error
}

const (
	FleetStatusFileRefType = "fleet_status"

	autoAOGReasonPrefixNextDue   = "[auto_next_due]"
	autoAOGReasonPrefixComplaint = "[auto_complaint]"
)

type NextDueEvaluation struct {
	InspectionType string
	AOG            bool
	Reason         string
}

type ComponentHours struct {
	Airframe float64
	Engine1  float64
	Engine2  float64
}

type FleetStatusService struct {
	repo        fleetstatus.Repository
	attachments fleetStatusAttachments
	helicopters fleetStatusHelicopters
	complaints  fleetStatusComplaints
	history     *FleetHistoryService
	usage       fleetStatusUsage
}

func (s *FleetStatusService) Create(ctx context.Context, row *fleetstatus.FleetStatus) error {
	if s.attachments == nil {
		if err := s.repo.Create(ctx, row); err != nil {
			return err
		}
		return s.syncAutomaticAOG(ctx, row)
	}
	if len(row.ID) == 0 {
		row.ID = uuidv7.MustBytes()
	}
	files, createdIDs, err := s.resolveFileLinks(ctx, row.ID, row.Files, nil, row.UpdatedBy)
	if err != nil {
		return err
	}
	row.Files = files
	if err := s.repo.Create(ctx, row); err != nil {
		s.rollbackAttachments(ctx, createdIDs)
		return err
	}
	if err := s.syncAutomaticAOG(ctx, row); err != nil {
		return err
	}
	return nil
}

func (s *FleetStatusService) Update(ctx context.Context, row *fleetstatus.FleetStatus) error {
	if s.attachments == nil {
		if err := s.repo.Update(ctx, row); err != nil {
			return err
		}
		return s.syncAutomaticAOG(ctx, row)
	}

	newKeys := make(map[string]struct{})
	for i := range row.Files {
		if len(row.Files[i].FileID) == 16 {
			key, _ := uuidv7.BytesToString(row.Files[i].FileID)
			newKeys[key] = struct{}{}
		}
	}
	if len(newKeys) == 0 {
		if err := s.repo.Update(ctx, row); err != nil {
			return err
		}
		return s.syncAutomaticAOG(ctx, row)
	}

	existing, err := s.repo.GetByID(ctx, row.ID)
	if err != nil {
		return err
	}
	reuse := make(map[string][]byte)
	type oldRef struct {
		key   string
		attID []byte
	}
	var olds []oldRef
	if existing != nil {
		for i := range existing.Files {
			ef := existing.Files[i]
			if ef.Attachment == nil || len(ef.Attachment.FileID) != 16 {
				continue
			}
			key, _ := uuidv7.BytesToString(ef.Attachment.FileID)
			reuse[key] = ef.FileAttachmentID
			olds = append(olds, oldRef{key: key, attID: ef.FileAttachmentID})
		}
	}

	files, createdIDs, err := s.resolveFileLinks(ctx, row.ID, row.Files, reuse, row.UpdatedBy)
	if err != nil {
		return err
	}
	row.Files = files

	if err := s.repo.Update(ctx, row); err != nil {
		s.rollbackAttachments(ctx, createdIDs)
		return err
	}

	if err := s.syncAutomaticAOG(ctx, row); err != nil {
		return err
	}

	for _, o := range olds {
		if _, keep := newKeys[o.key]; keep {
			continue
		}
		_ = s.attachments.DeleteAttachment(ctx, o.attID)
	}
	return nil
}

func (s *FleetStatusService) WithUsage(u fleetStatusUsage) *FleetStatusService {
	s.usage = u
	return s
}

func (s *FleetStatusService) ReviseSummary(ctx context.Context, helicopterID []byte, in helicopterusage.ManualSummaryInput, actor []byte) error {
	if s.usage == nil || len(helicopterID) != 16 {
		return nil
	}
	return s.usage.ReviseManualTotals(ctx, helicopterID, in, actor)
}

func (s *FleetStatusService) WithHistory(h *FleetHistoryService) *FleetStatusService {
	s.history = h
	return s
}

func (s *FleetStatusService) recordAutoAOG(ctx context.Context, heli *helicopter.Helicopter, set bool, reason string) {
	if s.history == nil || heli == nil {
		return
	}
	action := fleethistory.ActionAOGCleared
	msg := "AOG cleared (auto: maintenance no longer due)"
	if set {
		action = fleethistory.ActionAOGSet
		msg = "AOG set (auto: maintenance next due)"
		if r := strings.TrimSpace(reason); r != "" {
			msg = "AOG set (auto) — " + r
		}
	}
	if err := s.history.Record(ctx, heli.ID, fleethistory.EntityHelicopter, heli.ID, action, msg, nil); err != nil {
		slog.WarnContext(ctx, "fleet status: record auto-AOG history failed", slog.Any("error", err))
	}
}

func NewFleetStatusService(repo fleetstatus.Repository, attachments fleetStatusAttachments, helicopters fleetStatusHelicopters, complaints fleetStatusComplaints) *FleetStatusService {
	return &FleetStatusService{repo: repo, attachments: attachments, helicopters: helicopters, complaints: complaints}
}

func (s *FleetStatusService) syncAutomaticAOG(ctx context.Context, row *fleetstatus.FleetStatus) error {
	if s == nil || s.helicopters == nil || row == nil || len(row.HelicopterID) != 16 {
		return nil
	}
	heli, err := s.helicopters.GetByID(ctx, row.HelicopterID)
	if err != nil {
		return err
	}
	if heli == nil {
		idStr, _ := uuidv7.BytesToString(row.HelicopterID)
		slog.WarnContext(ctx, "fleet status: helicopter not found, skipping AOG sync", slog.String("helicopter_id", idStr))
		return nil
	}
	now := time.Now().UTC()

	var evals []NextDueEvaluation
	if hasEligibleSchedule(row.MaintenanceSchedules) {
		summary, sErr := s.GetSummaryByHelicopterID(ctx, row.HelicopterID)
		if sErr != nil {
			return sErr
		}
		hours := ComponentHours{
			Airframe: derefFloat(summary.Airframe.Hours),
			Engine1:  derefFloat(summary.Engine1.Hours),
			Engine2:  derefFloat(summary.Engine2.Hours),
		}
		evals, err = EvaluateNextDueSchedules(row.MaintenanceSchedules, hours, summary.Airframe.Hours != nil, heli.NR1, heli.NR2, now)
		if err != nil {
			return err
		}
	}

	loadComplaints := func() ([]complaint.Complaint, error) {
		if s.complaints == nil {
			return nil, nil
		}
		rows, _, lErr := s.complaints.ListByHelicopter(ctx, row.HelicopterID, 100, 0)
		return rows, lErr
	}
	grounded, reason, source, err := decideAutoAOG(evals, now, loadComplaints, func(c complaint.Complaint) string {
		return s.complaintAOGReason(ctx, c)
	})
	if err != nil {
		return err
	}
	return s.applyAutoAOG(ctx, heli, grounded, reason, source)
}

func decideAutoAOG(evals []NextDueEvaluation, now time.Time, loadComplaints func() ([]complaint.Complaint, error), complaintReason func(complaint.Complaint) string) (grounded bool, reason string, source string, err error) {
	for i := range evals {
		if !evals[i].AOG {
			continue
		}
		cleanReason := strings.TrimSpace(evals[i].Reason)
		if cleanReason == "" {
			cleanReason = fmt.Sprintf("%s next due reached", evals[i].InspectionType)
		}
		return true, autoAOGReasonPrefixNextDue + " " + cleanReason, helicopter.AOGSourceAuto, nil
	}

	complaints, err := loadComplaints()
	if err != nil {
		return false, "", "", err
	}
	for i := range complaints {
		if complaints[i].IsGrounding(now) {
			return true, complaintReason(complaints[i]), helicopter.AOGSourceComplaint, nil
		}
	}
	return false, "", "", nil
}

func (s *FleetStatusService) applyAutoAOG(ctx context.Context, heli *helicopter.Helicopter, grounded bool, reason, source string) error {
	if grounded {
		wasAOG := heli.AirOnGround
		if !heli.AirOnGround || strings.TrimSpace(heli.AOGReason) != reason || strings.TrimSpace(heli.AOGSource) != source {
			heli.AirOnGround = true
			heli.AOGReason = reason
			heli.AOGSource = source
			if err := s.helicopters.Update(ctx, heli); err != nil {
				return err
			}
			if !wasAOG {
				s.recordAutoAOG(ctx, heli, true, reason)
			}
		}
		return nil
	}

	if isAutoAOG(heli) {
		heli.AirOnGround = false
		heli.AOGReason = ""
		heli.AOGSource = helicopter.AOGSourceUnknown
		if err := s.helicopters.Update(ctx, heli); err != nil {
			return err
		}
		s.recordAutoAOG(ctx, heli, false, "")
	}
	return nil
}

func hasEligibleSchedule(schedules []fleetstatus.MaintenanceSchedule) bool {
	for i := range schedules {
		if strings.TrimSpace(schedules[i].Due) != "" || strings.TrimSpace(schedules[i].Ext) != "" {
			return true
		}
	}
	return false
}

func (s *FleetStatusService) OverdueInspections(ctx context.Context, helicopterID []byte) ([]fleetstatus.OverdueInspection, error) {
	if s == nil || s.helicopters == nil || len(helicopterID) != 16 {
		return nil, nil
	}
	latest, err := s.repo.LatestByHelicopterIDs(ctx, [][]byte{helicopterID})
	if err != nil {
		return nil, err
	}
	row := latest[string(helicopterID)]
	if row == nil || !hasEligibleSchedule(row.MaintenanceSchedules) {
		return nil, nil
	}
	heli, err := s.helicopters.GetByID(ctx, helicopterID)
	if err != nil || heli == nil {
		return nil, err
	}
	summary, err := s.GetSummaryByHelicopterID(ctx, helicopterID)
	if err != nil {
		return nil, err
	}
	hours := ComponentHours{
		Airframe: derefFloat(summary.Airframe.Hours),
		Engine1:  derefFloat(summary.Engine1.Hours),
		Engine2:  derefFloat(summary.Engine2.Hours),
	}
	evals, err := EvaluateNextDueSchedules(row.MaintenanceSchedules, hours, summary.Airframe.Hours != nil, heli.NR1, heli.NR2, time.Now().UTC())
	if err != nil {
		return nil, err
	}
	out := make([]fleetstatus.OverdueInspection, 0, len(evals))
	for i := range evals {
		if !evals[i].AOG {
			continue
		}
		detail := strings.TrimSpace(evals[i].Reason)
		if detail == "" {
			detail = evals[i].InspectionType + " next due reached"
		}
		out = append(out, fleetstatus.OverdueInspection{InspectionType: evals[i].InspectionType, Detail: detail})
	}
	return out, nil
}

func isAutoAOG(heli *helicopter.Helicopter) bool {
	if heli == nil || !heli.AirOnGround {
		return false
	}
	src := strings.TrimSpace(heli.AOGSource)
	if src == helicopter.AOGSourceAuto || src == helicopter.AOGSourceComplaint {
		return true
	}
	reason := strings.TrimSpace(heli.AOGReason)
	return strings.HasPrefix(reason, autoAOGReasonPrefixNextDue) || strings.HasPrefix(reason, autoAOGReasonPrefixComplaint)
}

func (s *FleetStatusService) complaintAOGReason(ctx context.Context, c complaint.Complaint) string {
	if c.MELSeverity > complaint.MELSeverityNonMEL {
		return autoAOGReasonPrefixComplaint + " MEL " + shareddto.MELLabel(c.MELSeverity) + " grace period expired"
	}
	if s.complaints != nil {
		if names, err := s.complaints.ResolveUserNames(ctx, []complaint.Complaint{c}); err == nil {
			if name := names[string(c.ReportedBy)]; name != "" {
				return autoAOGReasonPrefixComplaint + " NON-MEL complaint reported by " + name
			}
		}
	}
	return autoAOGReasonPrefixComplaint + " NON-MEL complaint reported"
}

func derefFloat(v *float64) float64 {
	if v == nil {
		return 0
	}
	return *v
}

func (s *FleetStatusService) resolveFileLinks(
	ctx context.Context,
	fleetStatusID []byte,
	files []fleetstatus.FleetStatusFile,
	reuse map[string][]byte,
	actor []byte,
) ([]fleetstatus.FleetStatusFile, [][]byte, error) {
	refID, _ := uuidv7.BytesToString(fleetStatusID)
	out := make([]fleetstatus.FleetStatusFile, 0, len(files))
	createdIDs := make([][]byte, 0, len(files))
	for i := range files {
		f := files[i]
		f.FleetStatusID = fleetStatusID
		if len(f.FileID) != 16 {
			if len(f.FileAttachmentID) == 16 {
				out = append(out, f)
			}
			continue
		}
		key, _ := uuidv7.BytesToString(f.FileID)
		if attID, ok := reuse[key]; ok {
			f.FileAttachmentID = attID
			f.FileID = nil
			out = append(out, f)
			continue
		}
		cat := f.Category
		att, err := s.attachments.CreateAttachment(ctx, filemanager.CreateAttachmentParams{
			FileID:   f.FileID,
			RefType:  FleetStatusFileRefType,
			RefID:    refID,
			Category: &cat,
			ActorID:  actor,
		})
		if err != nil {
			s.rollbackAttachments(ctx, createdIDs)
			return nil, nil, err
		}
		createdIDs = append(createdIDs, att.ID)
		f.FileAttachmentID = att.ID
		f.FileID = nil
		out = append(out, f)
	}
	return out, createdIDs, nil
}

func (s *FleetStatusService) rollbackAttachments(ctx context.Context, ids [][]byte) {
	for _, id := range ids {
		_ = s.attachments.DeleteAttachment(ctx, id)
	}
}

func (s *FleetStatusService) Delete(ctx context.Context, id []byte, deletedBy []byte) error {
	return s.repo.Delete(ctx, id, deletedBy)
}

func (s *FleetStatusService) GetByID(ctx context.Context, id []byte) (*fleetstatus.FleetStatus, error) {
	return s.repo.GetByID(ctx, id)
}

func (s *FleetStatusService) List(ctx context.Context, filter, sort string, limit, offset int) ([]fleetstatus.FleetStatus, int64, error) {
	return s.repo.List(ctx, filter, normalizeSortByRules(sort,
		sortField("created_at"),
		sortField("updated_at"),
	), limit, offset)
}

func (s *FleetStatusService) LatestByHelicopterIDs(ctx context.Context, helicopterIDs [][]byte) (map[string]*fleetstatus.FleetStatus, error) {
	return s.repo.LatestByHelicopterIDs(ctx, helicopterIDs)
}

func (s *FleetStatusService) RecalculateAOGByHelicopter(ctx context.Context, helicopterID []byte) error {
	if s == nil || len(helicopterID) != 16 {
		return nil
	}
	latest, err := s.repo.LatestByHelicopterIDs(ctx, [][]byte{helicopterID})
	if err != nil {
		return err
	}
	row := latest[string(helicopterID)]
	if row == nil {
		row = &fleetstatus.FleetStatus{HelicopterID: append([]byte(nil), helicopterID...)}
	}
	return s.syncAutomaticAOG(ctx, row)
}

func (s *FleetStatusService) RecalculateAllAOG(ctx context.Context) (int, error) {
	if s == nil {
		return 0, nil
	}
	ids, err := s.repo.HelicopterIDsWithFleetStatus(ctx)
	if err != nil {
		return 0, err
	}
	n := 0
	failed := 0
	for _, id := range ids {
		if err := s.RecalculateAOGByHelicopter(ctx, id); err != nil {
			failed++
			idStr, _ := uuidv7.BytesToString(id)
			slog.WarnContext(ctx, "fleet status AOG recompute failed for helicopter", slog.String("helicopter_id", idStr), slog.Any("error", err))
			continue
		}
		n++
	}
	if failed > 0 {
		slog.WarnContext(ctx, "fleet status AOG recompute completed with failures", slog.Int("succeeded", n), slog.Int("failed", failed))
	}
	return n, nil
}

func (s *FleetStatusService) RecalculateAOGByMission(ctx context.Context, missionID []byte) error {
	if s == nil || len(missionID) == 0 {
		return nil
	}
	helicopterID, err := s.repo.HelicopterIDByMissionID(ctx, missionID)
	if err != nil {
		return err
	}
	return s.RecalculateAOGByHelicopter(ctx, helicopterID)
}

func (s *FleetStatusService) MarkServiced(ctx context.Context, id []byte, servicedAt time.Time, actorID []byte) error {
	if servicedAt.IsZero() {
		servicedAt = time.Now().UTC()
	}
	if err := s.repo.MarkServiced(ctx, id, servicedAt.UTC(), actorID); err != nil {
		return err
	}
	row, err := s.repo.GetByID(ctx, id)
	if err != nil {
		return err
	}
	if row == nil {
		idStr, _ := uuidv7.BytesToString(id)
		slog.WarnContext(ctx, "fleet status: not found after mark-serviced, skipping AOG sync", slog.String("id", idStr))
		return nil
	}
	return s.syncAutomaticAOG(ctx, row)
}

func (s *FleetStatusService) ListServiceHistoryByHelicopterID(ctx context.Context, helicopterID []byte, limit, offset int) ([]fleetstatus.FleetStatusServiceLog, int64, error) {
	return s.repo.ListServiceHistoryByHelicopterID(ctx, helicopterID, limit, offset)
}

func (s *FleetStatusService) GetSummaryByHelicopterID(ctx context.Context, helicopterID []byte) (*shareddto.FleetStatusSummary, error) {
	if s.usage == nil || len(helicopterID) != 16 {
		return &shareddto.FleetStatusSummary{}, nil
	}
	usage, err := s.usage.GetByHelicopterID(ctx, helicopterID)
	if err != nil {
		return &shareddto.FleetStatusSummary{}, err
	}
	if usage == nil {
		return &shareddto.FleetStatusSummary{}, nil
	}
	hasE1, hasE2 := true, true
	if s.helicopters != nil {
		if heli, herr := s.helicopters.GetByID(ctx, helicopterID); herr == nil && heli != nil {
			hasE1, hasE2 = heli.NR1, heli.NR2
		}
	}
	summary := buildFleetStatusSummary(usage, hasE1, hasE2)
	return &summary, nil
}

func EvaluateNextDueSchedules(schedules []fleetstatus.MaintenanceSchedule, hours ComponentHours, hasUsageData, hasEngine1, hasEngine2 bool, now time.Time) ([]NextDueEvaluation, error) {
	parser := nextdue.NewParser()
	out := make([]NextDueEvaluation, 0, len(schedules))
	for i := range schedules {
		it := strings.TrimSpace(schedules[i].InspectionType)
		if (it == fleetstatus.InspectionTypeENG1 && !hasEngine1) || (it == fleetstatus.InspectionTypeENG2 && !hasEngine2) {
			out = append(out, NextDueEvaluation{InspectionType: it})
			continue
		}
		current := hours.Airframe
		switch it {
		case fleetstatus.InspectionTypeENG1:
			current = hours.Engine1
		case fleetstatus.InspectionTypeENG2:
			current = hours.Engine2
		}
		ev, err := evaluateInspectionDue(parser, it, schedules[i].Due, schedules[i].Ext, current, hasUsageData, now)
		if err != nil {
			return nil, err
		}
		out = append(out, ev)
	}
	return out, nil
}

func evaluateInspectionDue(parser nextdue.Parser, inspectionType, dueRaw, extRaw string, currentHours float64, hasUsageData bool, now time.Time) (NextDueEvaluation, error) {
	due, err := parser.Parse("due", dueRaw)
	if err != nil {
		return NextDueEvaluation{}, err
	}
	ext, err := parser.Parse("ext", extRaw)
	if err != nil {
		return NextDueEvaluation{}, err
	}
	ev := NextDueEvaluation{InspectionType: strings.TrimSpace(inspectionType)}

	limit := due
	limitIsExt := false
	if !nextdue.IsUnset(ext) {
		limit, limitIsExt = ext, true
	}
	if nextdue.IsUnset(limit) {
		return ev, nil
	}

	switch limit.Kind {
	case nextdue.ValueKindHours:
		if !hasUsageData {
			return ev, nil
		}
		if limit.Hours != nil && currentHours > *limit.Hours {
			ev.AOG = true
			ev.Reason = nextDueOverdueReason(inspectionType, limitIsExt, true)
		}
	case nextdue.ValueKindDate:
		if limit.Date != nil && nextdue.NormalizeDate(now).After(nextdue.NormalizeDate(*limit.Date)) {
			ev.AOG = true
			ev.Reason = nextDueOverdueReason(inspectionType, limitIsExt, false)
		}
	}
	return ev, nil
}

func nextDueOverdueReason(inspectionType string, isExt, isHours bool) string {
	it := strings.TrimSpace(inspectionType)
	switch {
	case isExt && isHours:
		return it + " extended hours reached"
	case isExt:
		return it + " extended due date reached"
	case isHours:
		return it + " due hours reached"
	default:
		return it + " due date reached"
	}
}

func buildFleetStatusSummary(u *helicopterusage.HelicopterUsage, hasE1, hasE2 bool) shareddto.FleetStatusSummary {
	airframeHours := u.TotalAirframeHours
	airframeCycles := float64(u.TotalAirframeCycles)
	landings := float64(u.TotalLanding)
	flightReports := float64(u.TotalFlightReport)
	rotorBrakeCycles := float64(u.TotalRotorBrakeCycle)
	hookReleases := float64(u.TotalHookRelease)
	summary := shareddto.FleetStatusSummary{
		Airframe: shareddto.FleetStatusSummaryAirframe{
			Hours:  &airframeHours,
			Cycles: &airframeCycles,
		},
		Landings:         &landings,
		FlightReports:    &flightReports,
		RotorBrakeCycles: &rotorBrakeCycles,
		HookReleases:     &hookReleases,
	}
	if hasE1 {
		summary.Engine1 = buildFleetStatusUsageEngine(u.TotalEngine1Hours, u.TotalEngine1GpcNgN1, u.TotalEngine1PtcNfN2, u.TotalEngine1Ccc)
	}
	if hasE2 {
		summary.Engine2 = buildFleetStatusUsageEngine(u.TotalEngine2Hours, u.TotalEngine2GpcNgN1, u.TotalEngine2PtcNfN2, u.TotalEngine2Ccc)
	}
	return summary
}

func buildFleetStatusUsageEngine(hours, gpcNgN1, ptcNfN2, ccc float64) shareddto.FleetStatusSummaryEngine {
	cccVal := ccc
	return shareddto.FleetStatusSummaryEngine{
		Hours:   &hours,
		GpcNgN1: &gpcNgN1,
		PtcNfN2: &ptcNfN2,
		Ccc:     &cccVal,
	}
}
