package salestracking

import (
	"context"
	"encoding/json"
	"errors"
	"time"

	"github.com/TaruLab/keluargamart-backend/internal/db"
)

// Sentinel errors mapped to HTTP in the handler.
var (
	ErrNotFound          = errors.New("not found")
	ErrNoEmployee        = errors.New("employee record required")
	ErrCustomerReq       = errors.New("customer_id is required")
	ErrVisitInvalid      = errors.New("visit is not checkable")
	ErrNotOwner          = errors.New("visit does not belong to you")
	ErrScheduleForbidden = errors.New("you cannot schedule this visit type")
)

// SchedulePermission carries the caller's fine-grained scheduling rights
// (resolved from auth.User.Can in the handler, kept out of this package to
// avoid an import cycle). Jadwal kunjungan penagihan (billing) is Finance's
// call; jadwal kunjungan rutin (routine) is SPV's call — see
// migrations/000060_visit_schedule_rbac.sql.
type SchedulePermission struct {
	CanBilling bool
	CanRoutine bool
}

// EmployeeLookup resolves an authenticated user id to their employee id.
type EmployeeLookup func(ctx context.Context, userID int64) (int64, error)

// Service implements sales-tracking use cases.
type Service struct {
	store  Store
	lookup EmployeeLookup
	loc    *time.Location
}

func NewService(store Store, loc *time.Location) *Service {
	return &Service{store: store, loc: loc}
}

// WithEmployeeLookup wires the user->employee resolver (main.go adapts the
// HR repository). Without it the service cannot bind sales_id for writes.
func (s *Service) WithEmployeeLookup(fn EmployeeLookup) *Service {
	if fn != nil {
		s.lookup = fn
	}
	return s
}

func (s *Service) employeeID(ctx context.Context, userID int64) (int64, error) {
	if s.lookup == nil {
		return 0, ErrNoEmployee
	}
	id, err := s.lookup(ctx, userID)
	if err != nil {
		return 0, ErrNoEmployee
	}
	return id, nil
}

// --- visits ------------------------------------------------------------------

func (s *Service) ListVisits(ctx context.Context, f Filter) ([]SalesVisit, int, error) {
	rows, total, err := s.store.ListVisits(ctx, f, nil)
	if err != nil {
		return nil, 0, err
	}
	out := make([]SalesVisit, 0, len(rows))
	for _, r := range rows {
		out = append(out, *visitFromListRow(r))
	}
	return out, int(total), nil
}

func (s *Service) MyVisits(ctx context.Context, userID int64, f Filter) ([]SalesVisit, int, error) {
	eid, err := s.employeeID(ctx, userID)
	if err != nil {
		return nil, 0, err
	}
	rows, total, err := s.store.ListVisits(ctx, f, &eid)
	if err != nil {
		return nil, 0, err
	}
	out := make([]SalesVisit, 0, len(rows))
	for _, r := range rows {
		out = append(out, *visitFromListRow(r))
	}
	return out, int(total), nil
}

func (s *Service) GetVisit(ctx context.Context, id int64) (*SalesVisit, error) {
	row, err := s.store.VisitByID(ctx, id)
	if err != nil {
		return nil, err
	}
	return visitFromRow(row), nil
}

// CreateVisit records a planned visit. The caller (eid) is always the
// scheduler on record; the visited sales rep defaults to the caller and may
// only be overridden by someone with billing/routine scheduling rights
// (Finance schedules "penagihan" visits, SPV schedules routine ones — a
// plain sales rep reaching this method has neither right, so in.SalesID and
// in.VisitType are simply ignored for them and the visit is routine/self).
func (s *Service) CreateVisit(ctx context.Context, userID int64, in VisitInput, perm SchedulePermission) (*SalesVisit, error) {
	// The SCHEDULER may be a back-office account with no employees row
	// (admin, or an SPV user created before HR onboarding): scheduled_by is
	// nullable for exactly that case, and the visit is anchored to the SALES
	// rep's employees row anyway. Resolving the caller before knowing whether
	// they are scheduling for themselves made every save from such an account
	// die with ErrNoEmployee → a 404 "Not found" on a visible button (2 Nov
	// flow audit, "jadwal kunjungan gagal not found saat simpan"). Resolve
	// lazily: only a SELF-schedule needs an employee record.
	eid, eidErr := s.employeeID(ctx, userID)
	if in.CustomerID == 0 {
		return nil, ErrCustomerReq
	}
	ok, err := s.store.CustomerExists(ctx, in.CustomerID)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, ErrCustomerReq
	}
	if in.VisitDate == "" {
		return nil, errors.New("visit_date is required")
	}
	if in.Purpose == "" {
		return nil, errors.New("purpose is required")
	}

	visitType := in.VisitType
	if visitType == "" {
		visitType = "routine"
	}
	if visitType != "routine" && visitType != "billing" {
		return nil, errors.New("visit_type must be routine or billing")
	}

	// A zero sales_id is an unset picker, not a target.
	if in.SalesID != nil && *in.SalesID == 0 {
		in.SalesID = nil
	}

	targetID := eid
	if in.SalesID != nil && *in.SalesID != eid {
		if !perm.CanBilling && !perm.CanRoutine {
			return nil, ErrScheduleForbidden
		}
		targetID = *in.SalesID
	} else if eidErr != nil {
		// Self-schedule: the visit anchors to the caller's own employees row,
		// which does not exist. Refuse with the specific error (422 at the
		// handler) instead of the generic Not found.
		return nil, ErrNoEmployee
	}
	switch visitType {
	case "billing":
		if !perm.CanBilling {
			return nil, ErrScheduleForbidden
		}
	case "routine":
		if targetID != eid && !perm.CanRoutine {
			return nil, ErrScheduleForbidden
		}
	}

	var scheduledBy *int64
	if eidErr == nil {
		scheduledBy = &eid
	}

	id, err := s.store.CreateVisit(ctx, db.CreateSalesVisitParams{
		SalesID:     int8(targetID),
		CustomerID:  int8(in.CustomerID),
		VisitDate:   dateOrNil(in.VisitDate),
		Purpose:     textOrNil(in.Purpose),
		Notes:       textOrNil(in.Notes),
		Status:      "planned",
		VisitType:   visitType,
		ScheduledBy: int8OrNil(scheduledBy),
	})
	if err != nil {
		return nil, err
	}
	return s.GetVisit(ctx, id)
}

// Checkin moves planned -> in_progress and records GPS (§9).
// isManager allows managers/supervisors/admins to check in on behalf of a sales rep.
func (s *Service) Checkin(ctx context.Context, userID, id int64, in GeoInput, isManager ...bool) (*SalesVisit, error) {
	v, err := s.GetVisit(ctx, id)
	if err != nil {
		return nil, err
	}
	manager := len(isManager) > 0 && isManager[0]
	if !manager {
		eid, err := s.employeeID(ctx, userID)
		if err != nil {
			return nil, err
		}
		if v.SalesID != eid {
			return nil, ErrNotOwner
		}
	}
	if v.Status != "planned" {
		return nil, ErrVisitInvalid
	}
	now := time.Now().In(s.loc)
	v.Status = "in_progress"
	v.CheckInTime = &now
	v.CheckInLat = in.Lat
	v.CheckInLng = in.Lng
	if in.Notes != "" {
		v.Notes = in.Notes
	}
	if in.Result != "" {
		v.Result = in.Result
	}
	if in.PhotoURL != "" {
		v.PhotoCheckIn = &in.PhotoURL
	}
	if err := s.updateVisit(ctx, v.SalesID, v); err != nil {
		return nil, err
	}
	return v, nil
}

// Checkout moves in_progress -> completed and records GPS; duration is
// derived from check_in_time/check_out_time (§9).
// isManager allows managers/supervisors/admins to check out on behalf of a sales rep.
func (s *Service) Checkout(ctx context.Context, userID, id int64, in GeoInput, isManager ...bool) (*SalesVisit, error) {
	v, err := s.GetVisit(ctx, id)
	if err != nil {
		return nil, err
	}
	manager := len(isManager) > 0 && isManager[0]
	if !manager {
		eid, err := s.employeeID(ctx, userID)
		if err != nil {
			return nil, err
		}
		if v.SalesID != eid {
			return nil, ErrNotOwner
		}
	}
	if v.Status != "in_progress" || v.CheckInTime == nil {
		return nil, ErrVisitInvalid
	}
	now := time.Now().In(s.loc)
	v.Status = "completed"
	v.CheckOutTime = &now
	v.CheckOutLat = in.Lat
	v.CheckOutLng = in.Lng
	if in.Notes != "" {
		v.Notes = in.Notes
	}
	if in.Result != "" {
		v.Result = in.Result
	}
	if in.PhotoURL != "" {
		v.PhotoCheckOut = &in.PhotoURL
	}
	if err := s.updateVisit(ctx, v.SalesID, v); err != nil {
		return nil, err
	}
	return v, nil
}

func (s *Service) updateVisit(ctx context.Context, salesID int64, v *SalesVisit) error {
	if err := s.store.UpdateVisit(ctx, db.UpdateSalesVisitParams{
		ID:           v.ID,
		SalesID:      int8(salesID),
		CheckInTime:  tsToPgtz(v.CheckInTime),
		CheckOutTime: tsToPgtz(v.CheckOutTime),
		CheckInLat:   floatToNd(v.CheckInLat),
		CheckInLng:   floatToNd(v.CheckInLng),
		CheckOutLat:  floatToNd(v.CheckOutLat),
		CheckOutLng:  floatToNd(v.CheckOutLng),
		Result:       textOrNil(v.Result),
		Notes:        textOrNil(v.Notes),
		Status:       v.Status,
	}); err != nil {
		return err
	}
	return s.store.UpdateVisitPhotos(ctx, v.ID, v.PhotoCheckIn, v.PhotoCheckOut)
}

// --- activities --------------------------------------------------------------

func (s *Service) ListActivities(ctx context.Context, f Filter) ([]SalesActivity, int, error) {
	rows, total, err := s.store.ListActivities(ctx, f, nil)
	if err != nil {
		return nil, 0, err
	}
	out := make([]SalesActivity, 0, len(rows))
	for _, r := range rows {
		out = append(out, *activityFromRow(r))
	}
	return out, int(total), nil
}

func (s *Service) CreateActivity(ctx context.Context, userID int64, in ActivityInput) (*SalesActivity, error) {
	eid, err := s.employeeID(ctx, userID)
	if err != nil && (in.SalesID == nil || *in.SalesID <= 0) {
		return nil, err
	}
	targetEID := eid
	if in.SalesID != nil && *in.SalesID > 0 {
		targetEID = *in.SalesID
	}
	if in.ActivityDate == "" {
		return nil, errors.New("activity_date is required")
	}
	if in.ActivityType == "" {
		return nil, errors.New("activity_type is required")
	}
	if in.Description == "" && in.Notes != "" {
		in.Description = in.Notes
	}

	// If customer_id is provided without sales_visit_id, automatically create a visit so customer is associated
	if in.SalesVisitID == nil && in.CustomerID != nil && *in.CustomerID > 0 {
		vID, vErr := s.store.CreateVisit(ctx, db.CreateSalesVisitParams{
			SalesID:     int8(targetEID),
			CustomerID:  int8(*in.CustomerID),
			VisitDate:   dateOrNil(in.ActivityDate),
			Purpose:     textOrNil("Aktivitas " + in.ActivityType),
			Notes:       textOrNil(in.Description),
			Status:      "completed",
			VisitType:   "routine",
			ScheduledBy: int8(targetEID),
		})
		if vErr == nil && vID > 0 {
			in.SalesVisitID = &vID
		}
	}

	id, err := s.store.CreateActivity(ctx, db.CreateSalesActivityParams{
		SalesID:       int8(targetEID),
		SalesVisitID:  int8OrNil(in.SalesVisitID),
		ActivityDate:  dateOrNil(in.ActivityDate),
		ActivityType:  in.ActivityType,
		Description:   textOrNil(in.Description),
		Result:        textOrNil(in.Result),
		FollowUpDate:  dateOrNil(strVal(in.FollowUpDate)),
		FollowUpNotes: textOrNil(in.FollowUpNotes),
	})
	if err != nil {
		return nil, err
	}
	return &SalesActivity{
		ID:            id,
		SalesID:       targetEID,
		SalesVisitID:  in.SalesVisitID,
		ActivityDate:  in.ActivityDate,
		ActivityType:  in.ActivityType,
		Description:   in.Description,
		Notes:         in.Description,
		Result:        in.Result,
		FollowUpDate:  in.FollowUpDate,
		FollowUpNotes: in.FollowUpNotes,
		Time:          time.Now().In(s.loc).Format("15:04"),
	}, nil
}

// --- routes ------------------------------------------------------------------

func (s *Service) ListRoutes(ctx context.Context, f Filter) ([]SalesRoute, int, error) {
	rows, total, err := s.store.ListRoutes(ctx, f, nil)
	if err != nil {
		return nil, 0, err
	}
	out := make([]SalesRoute, 0, len(rows))
	for _, r := range rows {
		out = append(out, *routeFromRow(r))
	}
	return out, int(total), nil
}

func (s *Service) MyRoutes(ctx context.Context, userID int64, f Filter) ([]SalesRoute, int, error) {
	eid, err := s.employeeID(ctx, userID)
	if err != nil {
		return nil, 0, err
	}
	rows, total, err := s.store.ListRoutes(ctx, f, &eid)
	if err != nil {
		return nil, 0, err
	}
	out := make([]SalesRoute, 0, len(rows))
	for _, r := range rows {
		out = append(out, *routeFromRow(r))
	}
	return out, int(total), nil
}

// CreateRoute stores the planned route; planned_customers JSON is required.
func (s *Service) CreateRoute(ctx context.Context, userID int64, in RouteInput) (*SalesRoute, error) {
	eid, err := s.employeeID(ctx, userID)
	if err != nil && (in.SalesID == nil || *in.SalesID <= 0) {
		return nil, err
	}
	targetEID := eid
	if in.SalesID != nil && *in.SalesID > 0 {
		targetEID = *in.SalesID
	}
	if in.Name == "" {
		return nil, errors.New("name is required")
	}
	if len(in.PlannedCustomers) == 0 {
		return nil, errors.New("planned_customers is required")
	}
	if in.RouteDate == "" {
		in.RouteDate = time.Now().In(s.loc).Format("2006-01-02")
	}
	raw, err := json.Marshal(in.PlannedCustomers)
	if err != nil {
		return nil, err
	}
	id, err := s.store.CreateRoute(ctx, db.CreateSalesRouteParams{
		SalesID:          int8(targetEID),
		RouteDate:        dateOrNil(in.RouteDate),
		Name:             textOrNil(in.Name),
		PlannedCustomers: raw,
		Status:           "planned",
		Notes:            textOrNil(in.Notes),
	})
	if err != nil {
		return nil, err
	}
	return &SalesRoute{
		ID:               id,
		SalesID:          targetEID,
		RouteDate:        in.RouteDate,
		Name:             in.Name,
		PlannedCustomers: in.PlannedCustomers,
		Status:           "planned",
		Notes:            in.Notes,
		LocationCount:    int64(len(in.PlannedCustomers)),
	}, nil
}

// --- locations ---------------------------------------------------------------

// PingLocations bulk-inserts high-frequency GPS pings (§9; excluded from the
// activity log). recorded_at defaults to now when omitted.
func (s *Service) PingLocations(ctx context.Context, userID int64, in []LocationPingInput) error {
	eid, err := s.employeeID(ctx, userID)
	if err != nil {
		return err
	}
	locs := make([]db.CreateSalesLocationParams, 0, len(in))
	for _, p := range in {
		rec := p.RecordedAt
		if rec == nil {
			t := time.Now().In(s.loc)
			rec = &t
		}
		locs = append(locs, db.CreateSalesLocationParams{
			SalesID:      int8(eid),
			Latitude:     floatToNd(&p.Latitude),
			Longitude:    floatToNd(&p.Longitude),
			Accuracy:     floatToNd(p.Accuracy),
			Speed:        floatToNd(p.Speed),
			Heading:      floatToNd(p.Heading),
			BatteryLevel: i2FromInt(p.BatteryLevel),
			RecordedAt:   tsToPgtz(rec),
		})
	}
	return s.store.InsertLocations(ctx, locs)
}

// CurrentLocations returns the latest recorded location per sales employee.
func (s *Service) CurrentLocations(ctx context.Context) ([]SalesLocation, error) {
	rows, err := s.store.LatestLocations(ctx)
	if err != nil {
		return nil, err
	}
	out := make([]SalesLocation, 0, len(rows))
	for _, r := range rows {
		out = append(out, locationFromRow(r))
	}
	return out, nil
}

// ListSalesEmployees serves the Finance/SPV "who am I scheduling for" picker
// on the jadwal kunjungan create form — {id, name} only.
func (s *Service) ListSalesEmployees(ctx context.Context) ([]SalesEmployeeRef, error) {
	rows, err := s.store.ListSalesEmployees(ctx)
	if err != nil {
		return nil, err
	}
	out := make([]SalesEmployeeRef, 0, len(rows))
	for _, r := range rows {
		out = append(out, SalesEmployeeRef{ID: r.ID, Name: r.Name})
	}
	return out, nil
}

// ListCustomerLocations serves the ERP tracking "Lokasi" tab: customer sites
// with aggregated visit counts.
func (s *Service) ListCustomerLocations(ctx context.Context, f Filter) ([]CustomerLocation, int, error) {
	page, per := normPage(f.Page, f.PerPage)
	rows, total, err := s.store.ListCustomerLocations(ctx, f.Search, f.City, per, (page-1)*per)
	if err != nil {
		return nil, 0, err
	}
	return rows, int(total), nil
}

func strVal(v *string) string {
	if v == nil {
		return ""
	}
	return *v
}
