package handlers

import (
	"errors"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"

	fleethistory "wucher/internal/domain/fleet_history"
	"wucher/internal/domain/mcf"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/shared/pkg/uuidv7"
	requestdto "wucher/internal/transport/http/dto/request"
	responsedto "wucher/internal/transport/http/dto/response"
	"wucher/internal/transport/http/jsonapi"
	"wucher/internal/transport/http/response"
	"wucher/internal/transport/http/validators"
)

type MCFHandler struct {
	svc        mcf.Service
	historySvc fleetHistoryRecorder
	validate   *validators.Validator
}

func NewMCFHandler(svc mcf.Service) *MCFHandler {
	return &MCFHandler{svc: svc, validate: validators.New()}
}

func (h *MCFHandler) WithHistoryService(svc fleetHistoryRecorder) *MCFHandler {
	h.historySvc = svc
	return h
}

func writeMCFServiceError(c *fiber.Ctx, err error, fallback apperrorsx.Def) error {
	if errors.Is(err, mcf.ErrNotFound) {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrMCFNotFound).WithCause(err))
	}
	return apperrorsx.Write(c, apperrorsx.New(fallback).WithCause(err))
}

func mcfResource(row *mcf.MaintenanceCheckFlight) responsedto.MCFResource {
	if row == nil {
		return responsedto.MCFResource{}
	}
	id, _ := uuidv7.BytesToString(row.ID)
	out := responsedto.MCFResource{
		Type: "mcf",
		ID:   id,
		Attributes: responsedto.MCFAttributes{
			HelicopterID: idString(row.HelicopterID),
			FlightID:     idString(row.FlightID),
			AFHours:      row.AFHours,
			LandingCount: row.LandingCount,
			DecidedBy:    idString(row.DecidedBy),
			Result:       row.Result,
			Notes:        row.Notes,
			CompletedBy:  idString(row.CompletedBy),
			IsComplete:   row.IsComplete(),
			IsCleared:    row.IsCleared(),
			IsPending:    row.IsPending(),
		},
	}
	if row.Date != nil && !row.Date.IsZero() {
		out.Attributes.Date = row.Date.UTC().Format("2006-01-02")
	}
	if row.UTCtime != nil && !row.UTCtime.IsZero() {
		v := row.UTCtime.UTC().Format(time.RFC3339)
		out.Attributes.UTCTime = &v
	}
	if row.DecidedAt != nil {
		v := row.DecidedAt.UTC().Format(time.RFC3339)
		out.Attributes.DecidedAt = &v
	}
	if row.CompletedAt != nil {
		v := row.CompletedAt.UTC().Format(time.RFC3339)
		out.Attributes.CompletedAt = &v
	}
	if !row.CreatedAt.IsZero() {
		out.Attributes.CreatedAt = row.CreatedAt.UTC().Format(time.RFC3339)
	}
	if !row.UpdatedAt.IsZero() {
		out.Attributes.UpdatedAt = row.UpdatedAt.UTC().Format(time.RFC3339)
	}
	return out
}

// Set godoc
// @Summary Set a helicopter's MCF Yes/No — draft work endpoint (create/update or cancel)
// @Description Toggles and edits a helicopter's MCF. `active:true` (or omitted) ACTIVATES/EDITS it — creates the draft if none is open (201), or UPDATES the open draft with the provided check-flight fields (200; af_hours, landing_count, utc_time, date, result, notes — all optional, partial edits allowed). Finalize it via POST /mcf/sign/{id} (sign:true). `active:false` DEACTIVATES it — cancels the open (unsigned) draft ("tidak jadi") and releases the MCF status; the cancelled record stays in the list. A signed MCF is a real event and is not cancelled.
// @Tags MCF
// @Accept json
// @Produce json
// @Param helicopter_id path string true "Helicopter ID (UUIDv7)"
// @Param request body requestdto.MCFSetRequest true "JSON:API MCF set request"
// @Success 201 {object} responsedto.MCFResponse
// @Success 200 {object} responsedto.MCFResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/mcf/set/{helicopter_id} [post]
func (h *MCFHandler) Set(c *fiber.Ctx) error {
	helicopterID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("helicopter_id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/helicopter_id"}
		return apperrorsx.Write(c, appErr)
	}
	var req requestdto.MCFSetRequest
	if err := c.BodyParser(&req); err != nil {
		return writeBodyParseError(c, err, apperrorsx.ErrMCFInvalidJSON)
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return jsonapi.WriteErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	attr := req.Data.Attributes
	active := attr.Active == nil || *attr.Active
	now := time.Now().UTC()
	actor := actorUserID(c)

	// Deactivate (No): cancel the helicopter's open (unsigned) draft → release MCF status.
	// A signed MCF is a real event and is left untouched.
	if !active {
		row, gerr := h.svc.GetLatestByHelicopter(c.UserContext(), helicopterID)
		if gerr != nil {
			return writeMCFServiceError(c, gerr, apperrorsx.ErrMCFCreateFailed)
		}
		if row == nil {
			// No MCF at all — already "No" (idempotent no-op).
			return response.Write(c, fiber.StatusOK, map[string]any{"data": map[string]any{"type": "mcf_set", "attributes": map[string]bool{"active": false}}})
		}
		if !row.IsOpen() {
			// Signed (or already cancelled) MCF is a real record — NOT cancelled by set:false.
			// Return it as-is so the client/fleet still shows the completed data.
			return response.Write(c, fiber.StatusOK, responsedto.MCFResponse{Data: mcfResource(row)})
		}
		row.CancelledAt = &now
		row.CancelledBy = actor
		row.UpdatedBy = actor
		if err := h.svc.Update(c.UserContext(), row); err != nil {
			return writeMCFServiceError(c, err, apperrorsx.ErrMCFCreateFailed)
		}
		updated, _ := h.svc.GetByID(c.UserContext(), row.ID)
		if updated == nil {
			updated = row
		}
		return response.Write(c, fiber.StatusOK, responsedto.MCFResponse{Data: mcfResource(updated)})
	}

	// Activate (Yes): the draft work endpoint — create the draft if none is open, or
	// UPDATE the open draft with the provided check-flight fields. Finalize via /mcf/sign.
	var flightID []byte
	if s := strings.TrimSpace(attr.FlightID); s != "" {
		flightID, err = uuidv7.ParseString(s)
		if err != nil {
			appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidUUID).WithCause(err)
			appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_id"}
			return apperrorsx.Write(c, appErr)
		}
	}
	draftFields := mcfSignFields{
		Result:       attr.Result,
		AFHours:      attr.AFHours,
		LandingCount: attr.LandingCount,
		UTCTime:      attr.UTCTime,
		Date:         attr.Date,
		Notes:        attr.Notes,
	}

	// Update the open draft in place if one exists.
	if existing, gerr := h.svc.GetLatestByHelicopter(c.UserContext(), helicopterID); gerr != nil {
		return writeMCFServiceError(c, gerr, apperrorsx.ErrMCFCreateFailed)
	} else if existing != nil && existing.IsOpen() {
		if aerr := applyMCFDraftFields(existing, draftFields); aerr != nil {
			return apperrorsx.Write(c, aerr)
		}
		if len(flightID) == 16 {
			existing.FlightID = flightID
		}
		existing.UpdatedBy = actor
		if err := h.svc.Update(c.UserContext(), existing); err != nil {
			return writeMCFServiceError(c, err, apperrorsx.ErrMCFCreateFailed)
		}
		updated, _ := h.svc.GetByID(c.UserContext(), existing.ID)
		if updated == nil {
			updated = existing
		}
		return response.Write(c, fiber.StatusOK, responsedto.MCFResponse{Data: mcfResource(updated)})
	}

	// No open draft → create a new one (with any provided data).
	row := &mcf.MaintenanceCheckFlight{
		HelicopterID: helicopterID,
		FlightID:     flightID,
		DecidedBy:    actor,
		DecidedAt:    &now,
		CreatedBy:    actor,
		UpdatedBy:    actor,
	}
	if aerr := applyMCFDraftFields(row, draftFields); aerr != nil {
		return apperrorsx.Write(c, aerr)
	}
	if err := h.svc.Create(c.UserContext(), row); err != nil {
		return writeMCFServiceError(c, err, apperrorsx.ErrMCFCreateFailed)
	}
	created, _ := h.svc.GetByID(c.UserContext(), row.ID)
	if created == nil {
		created = row
	}
	return response.Write(c, fiber.StatusCreated, responsedto.MCFResponse{Data: mcfResource(created)})
}

// GetByID godoc
// @Summary Get MCF by ID
// @Tags MCF
// @Produce json
// @Param id path string true "MCF ID (UUIDv7)"
// @Success 200 {object} responsedto.MCFResponse
// @Router /api/v1/mcf/get/{id} [get]
func (h *MCFHandler) GetByID(c *fiber.Ctx) error {
	id, err := uuidv7.ParseString(strings.TrimSpace(c.Params("id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/id"}
		return apperrorsx.Write(c, appErr)
	}
	row, err := h.svc.GetByID(c.UserContext(), id)
	if err != nil {
		return writeMCFServiceError(c, err, apperrorsx.ErrMCFNotFound)
	}
	if row == nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrMCFNotFound))
	}
	return response.Write(c, fiber.StatusOK, responsedto.MCFResponse{Data: mcfResource(row)})
}

// ListByHelicopter godoc
// @Summary List MCFs for a helicopter
// @Tags MCF
// @Produce json
// @Param helicopter_id path string true "Helicopter ID (UUIDv7)"
// @Success 200 {object} responsedto.MCFListResponse
// @Router /api/v1/mcf/get-by-helicopter/{helicopter_id} [get]
func (h *MCFHandler) ListByHelicopter(c *fiber.Ctx) error {
	helicopterID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("helicopter_id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/helicopter_id"}
		return apperrorsx.Write(c, appErr)
	}
	rows, err := h.svc.ListByHelicopter(c.UserContext(), helicopterID)
	if err != nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrMCFListFailed).WithCause(err))
	}
	data := make([]responsedto.MCFResource, 0, len(rows))
	for i := range rows {
		data = append(data, mcfResource(&rows[i]))
	}
	resp := responsedto.MCFListResponse{Data: data}
	resp.Meta.Total = int64(len(data))
	return response.Write(c, fiber.StatusOK, resp)
}

// mcfSignFields is the check-flight data recorded when an MCF is signed (completed).
type mcfSignFields struct {
	Result       string
	AFHours      string
	LandingCount *int
	UTCTime      string
	Date         string
	Notes        *string
}

// stampMCFSign validates the check-flight fields and stamps the row as signed/completed.
// Returns a non-nil AppError (422, pointing at the offending field) on invalid input.
func stampMCFSign(row *mcf.MaintenanceCheckFlight, f mcfSignFields, actor []byte) *apperrorsx.AppError {
	missing := ""
	switch {
	case strings.TrimSpace(f.Result) == "":
		missing = "result"
	case strings.TrimSpace(f.AFHours) == "":
		missing = "af_hours"
	case f.LandingCount == nil:
		missing = "landing_count"
	case strings.TrimSpace(f.UTCTime) == "":
		missing = "utc_time"
	case strings.TrimSpace(f.Date) == "":
		missing = "date"
	}
	if missing != "" {
		appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidPayload)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/" + missing}
		return appErr
	}
	result := strings.TrimSpace(f.Result)
	if result != mcf.ResultPassed && result != mcf.ResultFailed {
		appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidPayload)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/result"}
		return appErr
	}
	mcfDate, err := time.Parse("2006-01-02", strings.TrimSpace(f.Date))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidPayload).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/date"}
		return appErr
	}
	utcTime, err := time.Parse(time.RFC3339, strings.TrimSpace(f.UTCTime))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidPayload).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/utc_time"}
		return appErr
	}
	now := time.Now().UTC()
	d := mcfDate.UTC()
	utc := utcTime.UTC()
	row.Date = &d
	row.AFHours = strings.TrimSpace(f.AFHours)
	row.LandingCount = *f.LandingCount
	row.UTCtime = &utc
	row.Result = &result
	if f.Notes != nil {
		row.Notes = emptyToNilString(f.Notes)
	}
	row.CompletedAt = &now
	row.CompletedBy = append([]byte(nil), actor...)
	return nil
}

// applyMCFDraftFields saves the PROVIDED check-flight fields onto the row without
// completing it (a draft save — partial input allowed). Only non-empty fields are
// written; a bad date/utc_time/result → 422 pointing at the field.
func applyMCFDraftFields(row *mcf.MaintenanceCheckFlight, f mcfSignFields) *apperrorsx.AppError {
	if s := strings.TrimSpace(f.Date); s != "" {
		d, err := time.Parse("2006-01-02", s)
		if err != nil {
			appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidPayload).WithCause(err)
			appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/date"}
			return appErr
		}
		du := d.UTC()
		row.Date = &du
	}
	if s := strings.TrimSpace(f.UTCTime); s != "" {
		u, err := time.Parse(time.RFC3339, s)
		if err != nil {
			appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidPayload).WithCause(err)
			appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/utc_time"}
			return appErr
		}
		uu := u.UTC()
		row.UTCtime = &uu
	}
	if s := strings.TrimSpace(f.AFHours); s != "" {
		row.AFHours = s
	}
	if f.LandingCount != nil {
		row.LandingCount = *f.LandingCount
	}
	if s := strings.TrimSpace(f.Result); s != "" {
		if s != mcf.ResultPassed && s != mcf.ResultFailed {
			appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidPayload)
			appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/result"}
			return appErr
		}
		row.Result = &s
	}
	if f.Notes != nil {
		row.Notes = emptyToNilString(f.Notes)
	}
	return nil
}

// Sign godoc
// @Summary Sign (finalize) or unsign an MCF — toggle by MCF id
// @Description Finalizes an MCF's check-flight. `sign` (optional, default true): true FINALIZES it — af_hours, landing_count, utc_time, date, result required; "passed" clears the aircraft from MCF, "failed" keeps it grounded. false UNSIGNS it — reverts a signed MCF back to a draft (keeps the recorded data). Editing draft fields is done via /mcf/set; cancelling via /mcf/set (active:false).
// @Tags MCF
// @Accept json
// @Produce json
// @Param id path string true "MCF ID (UUIDv7)"
// @Param request body requestdto.MCFCompleteRequest true "JSON:API MCF sign request"
// @Success 200 {object} responsedto.MCFResponse
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/mcf/sign/{id} [post]
func (h *MCFHandler) Sign(c *fiber.Ctx) error {
	id, err := uuidv7.ParseString(strings.TrimSpace(c.Params("id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrMCFInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/id"}
		return apperrorsx.Write(c, appErr)
	}
	var req requestdto.MCFCompleteRequest
	if err := c.BodyParser(&req); err != nil {
		return writeBodyParseError(c, err, apperrorsx.ErrMCFInvalidJSON)
	}
	attr := req.Data.Attributes
	signWanted := attr.Sign == nil || *attr.Sign

	row, err := h.svc.GetByID(c.UserContext(), id)
	if err != nil {
		return writeMCFServiceError(c, err, apperrorsx.ErrMCFCompleteFailed)
	}
	if row == nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrMCFNotFound))
	}

	// Unsign (sign:false): revert a signed MCF back to a draft — clears the signature but
	// KEEPS the recorded data. Editing draft data is done via /mcf/set; cancelling via
	// /mcf/set (active:false).
	if !signWanted {
		row.CompletedAt = nil
		row.CompletedBy = nil
		row.UpdatedBy = actorUserID(c)
		if err := h.svc.Update(c.UserContext(), row); err != nil {
			return writeMCFServiceError(c, err, apperrorsx.ErrMCFCompleteFailed)
		}
		updated, _ := h.svc.GetByID(c.UserContext(), id)
		if updated == nil {
			updated = row
		}
		return response.Write(c, fiber.StatusOK, responsedto.MCFResponse{Data: mcfResource(updated)})
	}

	// Sign: the check-flight fields are required.
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return jsonapi.WriteErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if aerr := stampMCFSign(row, mcfSignFields{
		Result:       attr.Result,
		AFHours:      attr.AFHours,
		LandingCount: attr.LandingCount,
		UTCTime:      attr.UTCTime,
		Date:         attr.Date,
		Notes:        attr.Notes,
	}, actorUserID(c)); aerr != nil {
		return apperrorsx.Write(c, aerr)
	}
	row.UpdatedBy = actorUserID(c)
	if err := h.svc.Update(c.UserContext(), row); err != nil {
		return writeMCFServiceError(c, err, apperrorsx.ErrMCFCompleteFailed)
	}
	updated, _ := h.svc.GetByID(c.UserContext(), row.ID)
	if updated == nil {
		updated = row
	}
	if h.historySvc != nil {
		_ = h.historySvc.Record(c.UserContext(), updated.HelicopterID, fleethistory.EntityMCF, updated.ID, fleethistory.ActionMCFResult, "MCF result: "+strings.TrimSpace(attr.Result), actorUserID(c))
	}
	return response.Write(c, fiber.StatusOK, responsedto.MCFResponse{Data: mcfResource(updated)})
}

