package pdks

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

	"fiko-ai/pkg/id"
)

// Service, PDKS is kurallarini uygular: giris/cikis eslesmesi, sure hesabi,
// mola, mesai ve izin durumu.
type Service struct {
	repo Repository
	log  *slog.Logger
	now  func() time.Time
}

// Options, servis bagimliliklaridir.
type Options struct {
	Repo   Repository
	Logger *slog.Logger
	Now    func() time.Time
}

// New servis olusturur.
func New(o Options) *Service {
	if o.Logger == nil {
		o.Logger = slog.Default()
	}
	now := o.Now
	if now == nil {
		now = o.Repo.Now
	}
	return &Service{repo: o.Repo, log: o.Logger, now: now}
}

// Repo, depoyu dondurur (diger katmanlar test icin erisir).
func (s *Service) Repo() Repository { return s.repo }

func (s *Service) today() string { return DayOf(s.now()) }

// ---- bolum ----

// CreateDepartment yeni bolum olusturur. ParentID bos degilse var olmalidir ve
// DONGUSEL KURULAMAZ (a -> b -> a).
func (s *Service) CreateDepartment(ctx context.Context, d Department) (Department, error) {
	d.Name = strings.TrimSpace(d.Name)
	if d.Name == "" {
		return d, fmt.Errorf("%w: bolum adi zorunlu", ErrInvalid)
	}
	d.ID = strings.TrimSpace(d.ID)
	if d.ID == "" {
		d.ID = id.New("dep")
	}
	if p := strings.TrimSpace(d.ParentID); p != "" {
		if err := s.checkParentChain(ctx, d.ID, p); err != nil {
			return d, err
		}
	}
	now := s.now()
	d.CreatedAt, d.UpdatedAt = now, now
	return s.repo.CreateDepartment(ctx, d)
}

// checkParentChain, d.ID -> parent zincirinde dongu olmadigini dogrular.
func (s *Service) checkParentChain(ctx context.Context, selfID, parentID string) error {
	cur := parentID
	for i := 0; i < 32 && cur != ""; i++ { // 32 seviye tavani (dongu kombinasyonu)
		if cur == selfID {
			return fmt.Errorf("%w: bolum agacinda dongu olustu", ErrInvalid)
		}
		d, err := s.repo.GetDepartment(ctx, cur)
		if err != nil {
			return fmt.Errorf("%w: ust bolum bulunamadi (%s)", ErrInvalid, parentID)
		}
		cur = d.ParentID
	}
	if cur != "" {
		return fmt.Errorf("%w: bolum agaci cok derin", ErrInvalid)
	}
	return nil
}

func (s *Service) UpdateDepartment(ctx context.Context, d Department) (Department, error) {
	if strings.TrimSpace(d.ID) == "" {
		return d, fmt.Errorf("%w: bolum kimligi zorunlu", ErrInvalid)
	}
	if strings.TrimSpace(d.Name) == "" {
		return d, fmt.Errorf("%w: bolum adi zorunlu", ErrInvalid)
	}
	if p := strings.TrimSpace(d.ParentID); p != "" {
		if err := s.checkParentChain(ctx, d.ID, p); err != nil {
			return d, err
		}
	}
	d.UpdatedAt = s.now()
	return s.repo.UpdateDepartment(ctx, d)
}

// DeleteDepartment bolumu siler. Personeli varsa SILMEZ (veri butunlugu),
// onun yerine pasiflestirme onerilir.
func (s *Service) DeleteDepartment(ctx context.Context, id string) error {
	emps, err := s.repo.ListEmployees(ctx)
	if err != nil {
		return err
	}
	for _, e := range emps {
		if e.DepartmentID == id {
			return fmt.Errorf("%w: bu bolumde personel var, once personeli tasiyin veya bolumu pasiflestirin", ErrConflict)
		}
	}
	return s.repo.DeleteDepartment(ctx, id)
}

// ---- personel ----

func (s *Service) CreateEmployee(ctx context.Context, e Employee) (Employee, error) {
	e.Name = strings.TrimSpace(e.Name)
	if e.Name == "" {
		return e, fmt.Errorf("%w: personel adi zorunlu", ErrInvalid)
	}
	e.ID = strings.TrimSpace(e.ID)
	if e.ID == "" {
		e.ID = id.New("emp")
	}
	if d := strings.TrimSpace(e.DepartmentID); d != "" {
		if _, err := s.repo.GetDepartment(ctx, d); err != nil {
			return e, fmt.Errorf("%w: bolum bulunamadi (%s)", ErrInvalid, d)
		}
	}
	if u := strings.TrimSpace(e.UserID); u != "" {
		if existing, err := s.repo.GetEmployeeByUser(ctx, u); err == nil && existing.ID != e.ID {
			return e, fmt.Errorf("%w: bu sistem kullanicisi zaten %s personeline bagli", ErrDuplicate, existing.Name)
		}
	}
	now := s.now()
	e.CreatedAt, e.UpdatedAt = now, now
	return s.repo.CreateEmployee(ctx, e)
}

func (s *Service) UpdateEmployee(ctx context.Context, e Employee) (Employee, error) {
	if strings.TrimSpace(e.Name) == "" {
		return e, fmt.Errorf("%w: personel adi zorunlu", ErrInvalid)
	}
	if d := strings.TrimSpace(e.DepartmentID); d != "" {
		if _, err := s.repo.GetDepartment(ctx, d); err != nil {
			return e, fmt.Errorf("%w: bolum bulunamadi (%s)", ErrInvalid, d)
		}
	}
	if u := strings.TrimSpace(e.UserID); u != "" {
		if existing, err := s.repo.GetEmployeeByUser(ctx, u); err == nil && existing.ID != e.ID {
			return e, fmt.Errorf("%w: bu sistem kullanicisi zaten %s personeline bagli", ErrDuplicate, existing.Name)
		}
	}
	e.UpdatedAt = s.now()
	return s.repo.UpdateEmployee(ctx, e)
}

func (s *Service) DeleteEmployee(ctx context.Context, id string) error {
	return s.repo.DeleteEmployee(ctx, id)
}

// ---- vardiya ----

// CreateShift yeni vardiya olusturur. Start/End "HH:MM" olmali ve vardiya
// suresi 24 saati ASMAMALIDIR (capraz gecede 24 saate esit kabul edilir).
func (s *Service) CreateShift(ctx context.Context, sh Shift) (Shift, error) {
	if strings.TrimSpace(sh.Name) == "" {
		return sh, fmt.Errorf("%w: vardiya adi zorunlu", ErrInvalid)
	}
	if _, err := parseHM(sh.Start); err != nil {
		return sh, fmt.Errorf("%w: vardiya baslangici", ErrInvalid)
	}
	if _, err := parseHM(sh.End); err != nil {
		return sh, fmt.Errorf("%w: vardiya bitisi", ErrInvalid)
	}
	sh.ID = strings.TrimSpace(sh.ID)
	if sh.ID == "" {
		sh.ID = id.New("sft")
	}
	for _, d := range sh.Days {
		if d < 0 || d > 6 {
			return sh, fmt.Errorf("%w: gun 0-6 araliginda olmali", ErrInvalid)
		}
	}
	now := s.now()
	sh.CreatedAt, sh.UpdatedAt = now, now
	return s.repo.CreateShift(ctx, sh)
}

func (s *Service) UpdateShift(ctx context.Context, sh Shift) (Shift, error) {
	if _, err := parseHM(sh.Start); err != nil {
		return sh, fmt.Errorf("%w: vardiya baslangici", ErrInvalid)
	}
	if _, err := parseHM(sh.End); err != nil {
		return sh, fmt.Errorf("%w: vardiya bitisi", ErrInvalid)
	}
	sh.UpdatedAt = s.now()
	return s.repo.UpdateShift(ctx, sh)
}

func (s *Service) DeleteShift(ctx context.Context, id string) error {
	return s.repo.DeleteShift(ctx, id)
}

// ---- devam: giris / cikis / mola ----

// Punch, tek bir devam islemini ifade eder (model ve API bunu kullanir).
type Punch struct {
	EmpID  string
	Type   AttendanceType
	At     time.Time // sifirsa simdi
	Source string
	Note   string
}

// Punch, bir devam olayi kaydeder.
//
// DURUM DENETIMI (canli hatayi onleyen kural): ayni turda iki giris veya
// iki cikis OLMAZ. Yoksa hesap bozulur: 09:00 giris + 09:30 giris kaydi tutulursa
// "ilk giris" belirsizleşir ve sure yanlis hesaplanir. Kullanici 09:05'te
// yanlislikla kayit yaptiysa DUZELTME gerekir (Delete + yeniden Punch) — sessizce
// ustune yazmak veri butunlugu yaratir.
func (s *Service) Punch(ctx context.Context, p Punch) (Attendance, error) {
	if strings.TrimSpace(p.EmpID) == "" {
		return Attendance{}, fmt.Errorf("%w: personel zorunlu", ErrInvalid)
	}
	if _, err := s.repo.GetEmployee(ctx, p.EmpID); err != nil {
		return Attendance{}, fmt.Errorf("%w: personel bulunamadi", ErrNotFound)
	}
	at := p.At
	if at.IsZero() {
		at = s.now()
	}
	switch p.Type {
	case AttendanceIn, AttendanceOut, AttendanceBreakStart, AttendanceBreakEnd:
	default:
		return Attendance{}, fmt.Errorf("%w: bilinmeyen devam turu %q", ErrInvalid, p.Type)
	}

	// Ayni gun icindeki son olaya bak: durum gecisleri burada denetlenir.
	today := DayOf(at)
	evs, err := s.repo.ListAttendance(ctx, AttendFilter{EmpID: p.EmpID, From: today, To: today})
	if err != nil {
		return Attendance{}, err
	}
	if err := s.checkTransition(p.Type, evs); err != nil {
		return Attendance{}, err
	}

	// AYNI SANIYE ÇAKIŞMASI: iki kayıt (giriş→çıkış) aynı `at` değerini
	// alabilir (kullanıcı iki butona aynı anda basar, `at` verilmezse sunucu
	// saati kullanılır ve saniye çözünürlüğünde aynı görünür).
	//
	// CANLI HATA: bu durumda depo sıralaması ikinci anahtara (rastgele `_id`)
	// düşer; iki kayıt yer değiştirir ve durum denetimi YANLIŞ sırayla işler —
	// "giriş, çıkış, tekrar giriş" dizisi "çıkış, giriş, giriş" gibi görünür ve
	// ikinci giriş 409 döner (kullanıcı mesaiye başlayamaz).
	//
	// ÇÖZÜM: yeni kayıt mevcut en son kayıtla AYNI anda ise bir saniye ileri
	// alınır. Böylece kayıt SIRASI korunur (süre hesabı sıraya dayanır) ve
	// kullanıcının gerçek sırası bozulmaz.
	if n := len(evs); n > 0 && !at.After(evs[n-1].At) {
		at = evs[n-1].At.Add(time.Second)
		if d := DayOf(at); d != today {
			// Kayıt gün sınırını geçti: ertesi güne yazılır.
			today = d
		}
	}

	src := p.Source
	if src == "" {
		src = SourceManual
	}
	a := Attendance{
		ID:        id.New("att"),
		EmpID:     p.EmpID,
		Type:      p.Type,
		At:        at,
		DayOf:     today,
		Source:    src,
		Note:      strings.TrimSpace(p.Note),
		CreatedAt: s.now(),
	}
	return s.repo.CreateAttendance(ctx, a)
}

// checkTransition, yeni olayin mevcut olaylarla tutarli oldugunu dogrular.
//
// Kurallar:
//   - giris icin: acik bir giris ZATEN varsa hata (cift giris)
//   - cikis icin: hic giris yoksa hata (cift cikis / yalniz cikis)
//   - mola baslangici: giris var ve mola acik degilse olur
//   - mola bitisi: mola aciksa olur
func (s *Service) checkTransition(t AttendanceType, evs []Attendance) error {
	var inOpen, breakOpen bool
	for _, e := range evs { // zaman sirasina gore gelir (depo siraliyor)
		switch e.Type {
		case AttendanceIn:
			inOpen = true
			// giris, acik mola varsa onu KAPATIR (unutulmus mola).
			breakOpen = false
		case AttendanceOut:
			inOpen = false
			breakOpen = false
		case AttendanceBreakStart:
			breakOpen = true
		case AttendanceBreakEnd:
			breakOpen = false
		}
	}
	switch t {
	case AttendanceIn:
		if inOpen {
			return fmt.Errorf("%w: zaten giris yapilmis, once cikis yapin", ErrConflict)
		}
	case AttendanceOut:
		if !inOpen {
			return fmt.Errorf("%w: acik giris yok, once giris yapin", ErrConflict)
		}
	case AttendanceBreakStart:
		if !inOpen {
			return fmt.Errorf("%w: mola icin once giris yapin", ErrConflict)
		}
		if breakOpen {
			return fmt.Errorf("%w: zaten moladasiniz", ErrConflict)
		}
	case AttendanceBreakEnd:
		if !breakOpen {
			return fmt.Errorf("%w: acik mola yok", ErrConflict)
		}
	}
	return nil
}

// ---- hesaplama: gunluk ozet ----

// parseHM, "HH:MM" metnini dakikaya cevirir.
func parseHM(s string) (int, error) {
	t, err := time.Parse("15:04", strings.TrimSpace(s))
	if err != nil {
		return 0, err
	}
	return t.Hour()*60 + t.Minute(), nil
}

// formatHM, dakikayi "HH:MM" yapar.
func formatHM(min int) string {
	if min < 0 {
		min = 0
	}
	return fmt.Sprintf("%02d:%02d", min/60, min%60)
}

// shiftForDay, verilen gun icin uygulanacak vardiyayi bulur.
//
// Kural: Days listesi varsa yalniz o gunlerde gecerlidir; liste bossa HER GUN.
// En cok gun kactigi (belirsizlikte) vardiya secilir: iki vardiya ayni gun tanimli
// ise calisma saati hangisine gore sayilacaktir? Iki vardiya tanimlamak hatadir;
// secim yapabilmemiz gerekir ama belirsizligi de loglamamiz faydalidir.
func (s *Service) shiftForDay(ctx context.Context, day time.Time) (*Shift, error) {
	shifts, err := s.repo.ListShifts(ctx)
	if err != nil {
		return nil, err
	}
	idx := int(day.Weekday()) - 1 // time.Pzt=1 -> 0
	if idx < 0 {
		idx = 6 // Pazar
	}
	var best *Shift
	bestDays := -1
	for i := range shifts {
		sh := shifts[i]
		if !sh.Active {
			continue
		}
		if len(sh.Days) > 0 {
			hit := false
			for _, d := range sh.Days {
				if d == idx {
					hit = true
					break
				}
			}
			if !hit {
				continue
			}
		}
		if len(sh.Days) > bestDays {
			bestDays = len(sh.Days)
			cp := sh.Clone()
			best = &cp
		}
	}
	return best, nil
}

// Day, bir personelin TEK gununu hesaplar (raporun temel birimi).
//
// Hesap mantigi: gun icindeki OLAYLAR zaman sirasiyla islenir. Her "in" ile
// sonraki "out" arasindaki araliklar toplanir (aradaki molalar dusulur).
// Kapanmamis giris (gun hala suruyor) son ana kadar sayilir.
//
// DayOf bos ise bugun hesaplanir.
func (s *Service) Day(ctx context.Context, empID, dayOf string) (DailySummary, error) {
	if strings.TrimSpace(empID) == "" {
		return DailySummary{}, fmt.Errorf("%w: personel zorunlu", ErrInvalid)
	}
	if strings.TrimSpace(dayOf) == "" {
		dayOf = s.today()
	}
	emp, err := s.repo.GetEmployee(ctx, empID)
	if err != nil {
		return DailySummary{}, ErrNotFound
	}
	day, err := ParseDayOf(dayOf)
	if err != nil {
		return DailySummary{}, fmt.Errorf("%w: gun bicimi YYYY-MM-DD olmali", ErrInvalid)
	}
	evs, err := s.repo.ListAttendance(ctx, AttendFilter{EmpID: empID, From: dayOf, To: dayOf})
	if err != nil {
		return DailySummary{}, err
	}
	sum := DailySummary{EmpID: empID, DayOf: dayOf, Status: StatusAbsent}

	sh, _ := s.shiftForDay(ctx, day)
	if sh != nil {
		sum.ShiftID = sh.ID
	}

	// ---- izin kontrolu: onayli izin varsa gun "leave" sayilir ----
	leaves, _ := s.repo.ListLeave(ctx, empID, dayOf, dayOf)
	onLeave := false
	for _, l := range leaves {
		if l.Approved && l.From <= dayOf && l.To >= dayOf {
			onLeave = true
			sum.Note = "izinli: " + l.Kind
			break
		}
	}

	// ---- olaylari zaman sirasiyla isle ----
	var (
		openIn   *time.Time // acik giris
		breakAt  *time.Time // acik mola
		workMin  int
		breakMin int
		first    *time.Time
		last     *time.Time
	)
	for i := range evs {
		e := evs[i]
		at := e.At
		switch e.Type {
		case AttendanceIn:
			if openIn == nil {
				t := at
				openIn = &t
				first = &t
			}
			breakAt = nil // yeni giris unutulmus molayi kapatir
		case AttendanceBreakStart:
			if openIn != nil && breakAt == nil {
				t := at
				breakAt = &t
			}
		case AttendanceBreakEnd:
			if breakAt != nil {
				breakMin += int(at.Sub(*breakAt).Minutes())
				breakAt = nil
			}
		case AttendanceOut:
			if openIn != nil {
				workMin += int(at.Sub(*openIn).Minutes()) - breakMin
				t := at
				last = &t
				openIn = nil
				breakAt = nil
			}
		}
	}
	// Gun hala suruyor: acik giris son ana kadar sayilir.
	//
	// NOT: burada last ATANMAZ. Gun hala surerken last a su an yazilirsa
	// durum bolumu "acik giris + last var" gorunumune duser ve gun present
	// sayilir; oysa kaydi tamamlanmamis gun open olmali (canli testte tespit edildi).
	stillOpen := openIn != nil
	if openIn != nil {
		now := s.now()
		if now.Day() == day.Day() && now.Month() == day.Month() {
			workMin += int(now.Sub(*openIn).Minutes()) - breakMin
		}
	}

	if workMin < 0 {
		workMin = 0
	}

	sum.WorkMin = workMin
	sum.BreakMin = breakMin
	if first != nil {
		sum.FirstIn = first.Format("15:04")
	}
	if last != nil {
		sum.LastOut = last.Format("15:04")
	}

	// ---- durum ----
	switch {
	case onLeave && len(evs) == 0:
		sum.Status = StatusLeave
	case first != nil && stillOpen:
		sum.Status = StatusOpen // gun hala suruyor, cikis kaydi eksik
	case first != nil:
		sum.Status = StatusPresent
	case last != nil:
		sum.Status = StatusIncomplete // cikis var, giris yok
	default:
		sum.Status = StatusAbsent
	}

	// ---- vardiyaya gore gec kalma / erken cikis / mesai ----
	if sh != nil && first != nil {
		sMin, _ := parseHM(sh.Start)
		eMin, _ := parseHM(sh.End)
		fMin := first.Hour()*60 + first.Minute()
		// Capraz gecede (22:00 -> 06:00) baslangic saatindan KUCUK giris gec sayilir.
		if fMin < sMin && sMin > 12*60 {
			fMin += 24 * 60
		}
		if fMin > sMin {
			sum.LateMin = fMin - sMin
		}
		planned := eMin - sMin
		if planned < 0 {
			planned += 24 * 60
		}
		// Planli mola, PLANLANAN net sureden dusulur.
		//
		// Neden burada ve onceki "canli hata" notunda degil? Gercek mola sureyi
		// zaten dusmustur (workMin hesabinda). Vardiyanin BreakMin'i ise
		// PLANLANAN durustur: calisan 09:00-20:00 (11 saat) ise mesai
		// 11 - (9 saat vardiya - 1 saat planli mola) = 3 saattir. BreakMin
		// dusulmezse plan 9 saat olur ve mesai yanlis olarak 2 saat cikar
		// (canli test hatasi).
		planned -= sh.BreakMin
		// Mesai yalnizca vardiya suresi ASILDIIGINDA sayilir.
		//
		// Iki kosul birden gerekir (canli test hatasi):
		//  1. Erken cikis OLMAMALI. Aksi halde calisma az, "mesai" fazla gorunur.
		//  2. Geç kalma OLMAMALI. Geç kalma, vardiya suresini AŞAN ek calisma
		//     degildir; sadece baslangic kaymasidir. Vardiya 09:00-18:00, giris
		//     09:20, cikis 18:00 idi: net calisma 8:40, plan 8:00 -> 40 dk
		//     "mesai" deniyordu, oysa gercekte 20 dk gec kalma vardi ve mesai
		//     YOKTUR. Yoksa 3 saat gec kalen 8 saatlik vardiya 3 saat mesai
		//     gibi gorunur.
		if last != nil {
			lMin := last.Hour()*60 + last.Minute()
			if lMin < eMin && eMin > 12*60 {
				lMin += 24 * 60
			}
			if lMin < eMin {
				sum.EarlyMin = eMin - lMin
			}
			if sum.EarlyMin == 0 && sum.LateMin == 0 && workMin > planned {
				sum.OvertimeMin = workMin - planned
			}
		} else if sum.EarlyMin == 0 && sum.LateMin == 0 && workMin > planned {
			// Gun hala suruyor: ayni kural gecerli.
			sum.OvertimeMin = workMin - planned
		}
	}
	_ = emp
	return sum, nil
}

// ---- izin ----

// CreateLeave izin kaydi olusturur. Gun sayisi verilmezse hesaplanir
// (her gun 1,0; yarim gun izin icin 0,5 elle girilebilir).
func (s *Service) CreateLeave(ctx context.Context, l Leave) (Leave, error) {
	if strings.TrimSpace(l.EmpID) == "" {
		return l, fmt.Errorf("%w: personel zorunlu", ErrInvalid)
	}
	if _, err := s.repo.GetEmployee(ctx, l.EmpID); err != nil {
		return l, fmt.Errorf("%w: personel bulunamadi", ErrNotFound)
	}
	from, err := ParseDayOf(strings.TrimSpace(l.From))
	if err != nil {
		return l, fmt.Errorf("%w: izin baslangic tarihi gecersiz", ErrInvalid)
	}
	to, err := ParseDayOf(strings.TrimSpace(l.To))
	if err != nil {
		return l, fmt.Errorf("%w: izin bitis tarihi gecersiz", ErrInvalid)
	}
	if to.Before(from) {
		return l, fmt.Errorf("%w: izin bitisi baslangictan once olamaz", ErrInvalid)
	}
	if l.Kind == "" {
		l.Kind = LeaveAnnual
	}
	switch l.Kind {
	case LeaveAnnual, LeaveReport, LeaveSick, LeaveUnpaid, LeaveOther:
	default:
		return l, fmt.Errorf("%w: bilinmeyen izin turu %q", ErrInvalid, l.Kind)
	}
	if l.Days <= 0 {
		l.Days = to.Sub(from).Hours()/24 + 1
	}
	l.ID = strings.TrimSpace(l.ID)
	if l.ID == "" {
		l.ID = id.New("lve")
	}
	l.From, l.To = DayOf(from), DayOf(to)
	now := s.now()
	l.CreatedAt, l.UpdatedAt = now, now
	return s.repo.CreateLeave(ctx, l)
}

func (s *Service) UpdateLeave(ctx context.Context, l Leave) (Leave, error) {
	if _, err := ParseDayOf(strings.TrimSpace(l.From)); err != nil {
		return l, fmt.Errorf("%w: izin baslangic tarihi gecersiz", ErrInvalid)
	}
	if _, err := ParseDayOf(strings.TrimSpace(l.To)); err != nil {
		return l, fmt.Errorf("%w: izin bitis tarihi gecersiz", ErrInvalid)
	}
	l.UpdatedAt = s.now()
	return s.repo.UpdateLeave(ctx, l)
}

func (s *Service) DeleteLeave(ctx context.Context, id string) error {
	return s.repo.DeleteLeave(ctx, id)
}

// ---- raporlar ----

// Report, bir tarih araliginin toplamlaridir (haftalik/aylik devam raporu).
type Report struct {
	From string `json:"from"`
	To   string `json:"to"`
	Days int    `json:"days"` // araliktaki gun sayisi
	// Toplamlar araliktaki TUM gunler icindir.
	WorkMin     int `json:"workMin"`
	BreakMin    int `json:"breakMin"`
	LateMin     int `json:"lateMin"`
	OvertimeMin int `json:"overtimeMin"`
	Present     int `json:"present"`
	Absent      int `json:"absent"`
	Leave       int `json:"leave"`
	Open        int `json:"open"` // kaydi tamamlanmamis gun
	// Rows personel bazinda kirilimdir (rapor ekrani tablo gosterir).
	Rows []ReportRow `json:"rows"`
}

// ReportRow, bir personelin araliktaki ozetidir.
type ReportRow struct {
	EmpID       string `json:"empId"`
	Name        string `json:"name"`
	Dept        string `json:"dept,omitempty"`
	WorkMin     int    `json:"workMin"`
	BreakMin    int    `json:"breakMin"`
	LateMin     int    `json:"lateMin"`
	OvertimeMin int    `json:"overtimeMin"`
	Present     int    `json:"present"`
	Absent      int    `json:"absent"`
	Leave       int    `json:"leave"`
}

// Daily, tek personelin gunluk raporudur (arayuz tablosu).
type Daily struct {
	EmpID string       `json:"empId"`
	Name  string       `json:"name"`
	Dept  string       `json:"dept,omitempty"`
	Day   DailySummary `json:"day"`
}

// ReportRange, verilen araliktaki gun sayisini hesaplar (herkes icin ortak).
func (s *Service) Report(ctx context.Context, empID, from, to string) (Report, error) {
	from, to = strings.TrimSpace(from), strings.TrimSpace(to)
	if from == "" || to == "" {
		return Report{}, fmt.Errorf("%w: from ve to zorunlu (YYYY-MM-DD)", ErrInvalid)
	}
	f, err := ParseDayOf(from)
	if err != nil {
		return Report{}, fmt.Errorf("%w: from gecersiz", ErrInvalid)
	}
	t, err := ParseDayOf(to)
	if err != nil {
		return Report{}, fmt.Errorf("%w: to gecersiz", ErrInvalid)
	}
	if t.Before(f) {
		return Report{}, fmt.Errorf("%w: to, from'dan once olamaz", ErrInvalid)
	}
	// Aralik siniri: 366 gun. Aksi halde bir model/istemci 10 yillik aralik isteyip
	// bellek ve sure tuketir (hizmet reddi).
	if t.Sub(f).Hours()/24 > 366 {
		return Report{}, fmt.Errorf("%w: en fazla 366 gunluk aralik sorgulanabilir", ErrInvalid)
	}

	emps, err := s.repo.ListEmployees(ctx)
	if err != nil {
		return Report{}, err
	}
	depts, _ := s.repo.ListDepartments(ctx)
	deptName := map[string]string{}
	for _, d := range depts {
		deptName[d.ID] = d.Name
	}

	rep := Report{From: from, To: to, Days: int(t.Sub(f).Hours()/24) + 1}
	for _, e := range emps {
		if empID != "" && e.ID != empID {
			continue
		}
		row := ReportRow{EmpID: e.ID, Name: e.Name, Dept: deptName[e.DepartmentID]}
		for d := f; !d.After(t); d = d.AddDate(0, 0, 1) {
			sum, err := s.Day(ctx, e.ID, DayOf(d))
			if err != nil {
				continue
			}
			rep.WorkMin += sum.WorkMin
			rep.BreakMin += sum.BreakMin
			rep.LateMin += sum.LateMin
			rep.OvertimeMin += sum.OvertimeMin
			row.WorkMin += sum.WorkMin
			row.BreakMin += sum.BreakMin
			row.LateMin += sum.LateMin
			row.OvertimeMin += sum.OvertimeMin
			switch sum.Status {
			case StatusPresent:
				rep.Present++
				row.Present++
			case StatusAbsent:
				rep.Absent++
				row.Absent++
			case StatusLeave:
				rep.Leave++
				row.Leave++
			case StatusOpen, StatusIncomplete:
				rep.Open++
			}
		}
		rep.Rows = append(rep.Rows, row)
	}
	sort.Slice(rep.Rows, func(i, j int) bool { return rep.Rows[i].Name < rep.Rows[j].Name })
	return rep, nil
}

// DailyRange, bir gunun tum personel raporudur (gunluk devam tablosu).
func (s *Service) DailyRange(ctx context.Context, dayOf string) ([]Daily, error) {
	if strings.TrimSpace(dayOf) == "" {
		dayOf = s.today()
	}
	emps, err := s.repo.ListEmployees(ctx)
	if err != nil {
		return nil, err
	}
	depts, _ := s.repo.ListDepartments(ctx)
	deptName := map[string]string{}
	for _, d := range depts {
		deptName[d.ID] = d.Name
	}
	out := make([]Daily, 0, len(emps))
	for _, e := range emps {
		sum, err := s.Day(ctx, e.ID, dayOf)
		if err != nil {
			continue
		}
		out = append(out, Daily{EmpID: e.ID, Name: e.Name, Dept: deptName[e.DepartmentID], Day: sum})
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
	return out, nil
}

// GetShift vardiya getirir (ErrNotFound).
func (s *Service) GetShift(ctx context.Context, id string) (Shift, error) {
	shifts, err := s.repo.ListShifts(ctx)
	if err != nil {
		return Shift{}, err
	}
	for _, sh := range shifts {
		if sh.ID == id {
			return sh, nil
		}
	}
	return Shift{}, ErrNotFound
}

// ListShifts tum vardiyalari dondurur.
func (s *Service) ListShifts(ctx context.Context) ([]Shift, error) {
	return s.repo.ListShifts(ctx)
}

// ListDepartments tum bolumleri dondurur.
func (s *Service) ListDepartments(ctx context.Context) ([]Department, error) {
	return s.repo.ListDepartments(ctx)
}

// GetDepartment bolum getirir.
func (s *Service) GetDepartment(ctx context.Context, id string) (Department, error) {
	return s.repo.GetDepartment(ctx, id)
}

// ListEmployees tum personeli dondurur.
func (s *Service) ListEmployees(ctx context.Context) ([]Employee, error) {
	return s.repo.ListEmployees(ctx)
}

// GetEmployee personel getirir.
func (s *Service) GetEmployee(ctx context.Context, id string) (Employee, error) {
	return s.repo.GetEmployee(ctx, id)
}

// DeleteAttendance TEK bir devam olayini siler (DUZELTME amacli).
//
// Neden gerekli? Personel yanlis kayit yaptiginda (09:05 yerine 09:50 girdi)
// en dogru cozum kaydi silip yeniden eklemektir. Ustune yazmak denetimi bozar.
// Bu uc yoneticiye aciktir ve denetim kaydi yazar.
func (s *Service) DeleteAttendance(ctx context.Context, id string) error {
	return s.repo.DeleteAttendance(ctx, id)
}

// ListAttendance, filtreye uyan devam olaylarini dondurur (arayuz odakli sorgular).
func (s *Service) ListAttendance(ctx context.Context, f AttendFilter) ([]Attendance, error) {
	return s.repo.ListAttendance(ctx, f)
}

// ListLeave, personel izinlerini dondurur.
func (s *Service) ListLeave(ctx context.Context, empID, from, to string) ([]Leave, error) {
	return s.repo.ListLeave(ctx, empID, from, to)
}
