package claim

// Proje + görev modülü ve hazır raporlar (Faz 9).
//
// Proje ve görev ayrı bir sistem DEĞİL, aynı `claims` yapısındaki tipli kayıtlardır (şemalar
// internal/claim/kinds). Bu dosya onların üzerinde hazır raporları üretir:
//
//	gecikmis_gorev  son tarihi geçmiş, tamamlanmamış tüm görevler
//	proje_durumu    tek bir projenin durumu, ilerleme yüzdesi, görevleri
//	proje_ozet      tüm projelerin tek bakışta özeti
//
// KURAL (spec, firma yönetimi modülleri): HESAPLAMA KODDA YAPILIR, model yapmaz. Gecikme günü,
// ilerleme yüzdesi, geciken görev sayısı burada hesaplanır; rapor metni sunucuda üretilir ve cevabın
// ardına olduğu gibi eklenir (liste sorularındaki gibi). Model yalnızca tek bir giriş cümlesi yazar.
//
// Proje ile görev arasındaki bağ görevin `proje` alanıdır (projenin adı). Eşleşme ad normalleştirilerek
// yapılır ("B-12" ↔ "b 12"); bağ kurulamayan görevler "projesiz" sayılır, kaybolmaz.

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"
	"unicode/utf8"
)

// ReportType hazır rapor türüdür.
type ReportType string

const (
	ReportOverdue ReportType = "gecikmis_gorev"
	ReportProject ReportType = "proje_durumu"
	ReportSummary ReportType = "proje_ozet"
)

// Valid bilinen bir rapor türü mü?
func (t ReportType) Valid() bool {
	switch t {
	case ReportOverdue, ReportProject, ReportSummary:
		return true
	}
	return false
}

// ReportRequest istenen rapordur. Project yalnızca ReportProject için anlamlıdır.
type ReportRequest struct {
	Type    ReportType `json:"type"`
	Project string     `json:"project,omitempty"`
}

// trt Türkiye saatidir (UTC+3; 2016'dan beri yaz saati yok). "Bugün" bu saate göre hesaplanır:
// tarih alanları takvim günüdür, sunucunun saat dilimine bağlı olmamalıdır.
var trt = time.FixedZone("TRT", 3*3600)

// Today verilen anın Türkiye takvimindeki gününü YYYY-MM-DD olarak döner.
func Today(now time.Time) string { return now.In(trt).Format("2006-01-02") }

// dayDiff b - a gün farkıdır (iki YYYY-MM-DD). Çözülemezse ok=false.
func dayDiff(a, b string) (int, bool) {
	ta, err1 := time.Parse("2006-01-02", a)
	tb, err2 := time.Parse("2006-01-02", b)
	if err1 != nil || err2 != nil {
		return 0, false
	}
	return int(tb.Sub(ta).Hours() / 24), true
}

// Görev ve proje durumları (kinds şemasındaki enum değerleri).
func taskOpen(status string) bool {
	switch status {
	case "", "acik", "devam", "beklemede":
		return true
	}
	return false
}

// TaskInfo bir görevin hesaplanmış görünümüdür.
type TaskInfo struct {
	ID       string `json:"id"`
	Text     string `json:"text"`
	Project  string `json:"project,omitempty"`
	Assignee string `json:"assignee,omitempty"`
	Due      string `json:"due,omitempty"`
	Status   string `json:"status"`
	Open     bool   `json:"open"`
	Overdue  bool   `json:"overdue"`
	DaysLate int    `json:"daysLate,omitempty"` // yalnızca Overdue ise
	Author   string `json:"author,omitempty"`
	// Conflict kaydın çelişkili işaretli olduğunu bildirir (iki farklı bilgi var; patron karar verir).
	Conflict bool `json:"conflict,omitempty"`
}

// ProjectInfo bir projenin hesaplanmış görünümüdür.
type ProjectInfo struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Owner    string `json:"owner,omitempty"`
	Status   string `json:"status,omitempty"`
	Priority string `json:"priority,omitempty"`
	Start    string `json:"start,omitempty"`
	End      string `json:"end,omitempty"`
	Conflict bool   `json:"conflict,omitempty"`

	Total     int `json:"total"`     // iptal edilenler HARİÇ görev sayısı
	Done      int `json:"done"`      // tamamlanan
	Open      int `json:"open"`      // açık (acik/devam/beklemede)
	Cancelled int `json:"cancelled"` // iptal
	Overdue   int `json:"overdue"`   // gecikmiş (açık + son tarihi geçmiş)
	// Progress tamamlanan / (iptal hariç toplam) yüzdesidir; görevi olmayan projede -1.
	Progress int `json:"progress"`
	// DaysToEnd bitişe kalan gün (negatif = geçti); EndKnown=false ise bitiş tarihi yok.
	DaysToEnd int  `json:"daysToEnd,omitempty"`
	EndKnown  bool `json:"endKnown,omitempty"`
	// Late proje açıkken bitiş tarihi geçmiş.
	Late  bool       `json:"late,omitempty"`
	Tasks []TaskInfo `json:"tasks"`
}

// ReportResult bir raporun hesaplanmış sonucudur. Text kullanıcıya OLDUĞU GİBİ gösterilir.
type ReportResult struct {
	Type    ReportType `json:"type"`
	Project string     `json:"project,omitempty"` // istenen ad (proje_durumu)
	Today   string     `json:"today"`

	// Overdue raporunda gecikmiş görevler (en çok geciken önce); OpenTasks açık görev sayısı;
	// NoDue son tarihi olmayan açık görev sayısı.
	Tasks     []TaskInfo `json:"tasks,omitempty"`
	OpenTasks int        `json:"openTasks,omitempty"`
	NoDue     int        `json:"noDue,omitempty"`

	Projects []ProjectInfo `json:"projects,omitempty"`
	// Unassigned projeye bağlanamayan görevler (özet raporunda sayılır).
	Unassigned []TaskInfo `json:"unassigned,omitempty"`
	NotFound   bool       `json:"notFound,omitempty"` // istenen proje yok
	Text       string     `json:"text"`
}

const maxReportText = 200 // rapor satırında görev metni üst sınırı (rune)

// sameProject iki normalleştirilmiş proje adı aynı projeyi mi gösteriyor? Birebir eşitlik ya da
// (kısa olan en az 3 karakterse) bir adın diğerinin içinde TAM SÖZCÜK dizisi olarak geçmesi:
// "b 12" ↔ "b 12 kalip" evet; "b 1" ↔ "b 12" hayır.
func sameProject(a, b string) bool {
	if a == "" || b == "" {
		return false
	}
	if a == b || strings.ReplaceAll(a, " ", "") == strings.ReplaceAll(b, " ", "") {
		return true // "b 12" ↔ "b12"
	}
	short, long := a, b
	if len(short) > len(long) {
		short, long = long, short
	}
	if utf8.RuneCountInString(short) < 3 {
		return false
	}
	return strings.Contains(" "+long+" ", " "+short+" ")
}

func projectKey(c Claim) string {
	if c.Key != "" {
		return c.Key
	}
	return NormalizeKey(c.Fields["ad"])
}

func projectName(c Claim) string {
	if n := strings.TrimSpace(c.Fields["ad"]); n != "" {
		return n
	}
	return c.Text
}

func taskInfo(c Claim, today string) TaskInfo {
	t := TaskInfo{
		ID: c.ID, Text: c.Text, Project: c.Fields["proje"], Assignee: c.Fields["atanan"],
		Due: c.Fields["son_tarih"], Status: c.Fields["durum"], Conflict: c.Status == StatusConflict,
		Author: c.AuthorName,
	}
	if t.Status == "" {
		t.Status = "acik"
	}
	t.Open = taskOpen(t.Status)
	if t.Open && t.Due != "" {
		if d, ok := dayDiff(t.Due, today); ok && d > 0 {
			t.Overdue, t.DaysLate = true, d
		}
	}
	return t
}

// taskBelongs görev bu projeye ait mi? Önce görevin `proje` alanı; alan boşsa görev metninde projenin
// adı tam sözcük olarak geçiyor mu.
func taskBelongs(t TaskInfo, pkey string) bool {
	if k := NormalizeKey(t.Project); k != "" {
		return sameProject(pkey, k)
	}
	k := NormalizeKey(t.Text)
	return utf8.RuneCountInString(pkey) >= 3 && strings.Contains(" "+k+" ", " "+pkey+" ")
}

func sortTasks(ts []TaskInfo) {
	sort.SliceStable(ts, func(i, j int) bool {
		a, b := ts[i], ts[j]
		if a.Overdue != b.Overdue {
			return a.Overdue
		}
		if a.Overdue { // en çok geciken önce
			if a.DaysLate != b.DaysLate {
				return a.DaysLate > b.DaysLate
			}
		}
		if a.Open != b.Open {
			return a.Open // açıklar önce
		}
		if (a.Due == "") != (b.Due == "") {
			return a.Due != "" // tarihlisi tarihsizden önce
		}
		return a.Due < b.Due
	})
}

func buildProject(c Claim, tasks []TaskInfo, today string) ProjectInfo {
	p := ProjectInfo{
		ID: c.ID, Name: projectName(c), Owner: c.Fields["sorumlu"], Status: c.Fields["durum"],
		Priority: c.Fields["oncelik"], Start: c.Fields["baslangic"], End: c.Fields["bitis"],
		Conflict: c.Status == StatusConflict, Tasks: tasks, Progress: -1,
	}
	for _, t := range tasks {
		switch {
		case t.Status == "iptal":
			p.Cancelled++
			continue
		case t.Status == "tamamlandi":
			p.Done++
		case t.Open:
			p.Open++
		}
		p.Total++
		if t.Overdue {
			p.Overdue++
		}
	}
	if p.Total > 0 {
		p.Progress = (p.Done*100 + p.Total/2) / p.Total // yuvarlama (tamsayı)
	}
	if p.End != "" {
		if d, ok := dayDiff(today, p.End); ok {
			p.DaysToEnd, p.EndKnown = d, true
			p.Late = d < 0 && p.Status != "tamamlandi" && p.Status != "iptal"
		}
	}
	return p
}

// listAll bir süzgece uyan tüm kayıtları sayfalayarak okur.
func listAll(ctx context.Context, repo Repository, f Filter, size int) ([]Claim, error) {
	if size <= 0 {
		size = defaultPageSize
	}
	var out []Claim
	for off := 0; ; off += size {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		page, err := repo.List(ctx, f, size, off)
		if err != nil {
			return nil, err
		}
		out = append(out, page...)
		if len(page) < size {
			return out, nil
		}
	}
}

// BuildReport hazır raporu hesaplar. Okuyan kişinin görme sınırı (Actor.Scope) depoya iletilir;
// proje ve görev ekip türleridir, ancak süzgeç yine uygulanır (ileride bir tür özel olursa rapor sızdırmaz).
// `guncellendi` ve `gecersiz` kayıtlar hesaba girmez; çelişkili kayıtlar girer ve işaretlenir.
func BuildReport(ctx context.Context, repo Repository, req ReportRequest, actor Actor, now time.Time, pageSize int) (*ReportResult, error) {
	if !req.Type.Valid() {
		return nil, fmt.Errorf("claim: bilinmeyen rapor türü %q", req.Type)
	}
	today := Today(now)
	res := &ReportResult{Type: req.Type, Project: req.Project, Today: today}

	taskClaims, err := listAll(ctx, repo, actor.Scope(Filter{Kinds: []string{KindGorev}, Statuses: visibleStatuses()}), pageSize)
	if err != nil {
		return nil, err
	}
	tasks := make([]TaskInfo, 0, len(taskClaims))
	for _, c := range taskClaims {
		tasks = append(tasks, taskInfo(c, today))
	}

	if req.Type == ReportOverdue {
		for _, t := range tasks {
			if t.Open {
				res.OpenTasks++
				if t.Due == "" {
					res.NoDue++
				}
			}
			if t.Overdue {
				res.Tasks = append(res.Tasks, t)
			}
		}
		sortTasks(res.Tasks)
		res.Text = overdueText(res)
		return res, nil
	}

	projClaims, err := listAll(ctx, repo, actor.Scope(Filter{Kinds: []string{"proje"}, Statuses: visibleStatuses()}), pageSize)
	if err != nil {
		return nil, err
	}

	// Her projenin görevlerini bağla; hiçbir projeye bağlanamayanlar "projesiz".
	assigned := make(map[string]bool, len(tasks))
	var all []ProjectInfo
	for _, pc := range projClaims {
		pkey := projectKey(pc)
		var own []TaskInfo
		for _, t := range tasks {
			if taskBelongs(t, pkey) {
				own = append(own, t)
				assigned[t.ID] = true
			}
		}
		sortTasks(own)
		all = append(all, buildProject(pc, own, today))
	}
	for _, t := range tasks {
		if !assigned[t.ID] {
			res.Unassigned = append(res.Unassigned, t)
		}
	}
	sortTasks(res.Unassigned)

	if req.Type == ReportSummary {
		sort.SliceStable(all, func(i, j int) bool {
			if all[i].Overdue != all[j].Overdue {
				return all[i].Overdue > all[j].Overdue // en çok geciken önde
			}
			return NormalizeKey(all[i].Name) < NormalizeKey(all[j].Name)
		})
		res.Projects = all
		res.Text = summaryText(res)
		return res, nil
	}

	// ReportProject: istenen adı bul. Birebir eşleşme varsa yalnızca onlar, yoksa kapsayan eşleşmeler.
	want := NormalizeKey(req.Project)
	var exact, loose []ProjectInfo
	for i, p := range all {
		k := projectKey(projClaims[i])
		switch {
		case k == want && want != "":
			exact = append(exact, p)
		case sameProject(k, want):
			loose = append(loose, p)
		}
	}
	found := exact
	if len(found) == 0 {
		found = loose
	}
	if len(found) == 0 {
		res.NotFound = true
		names := make([]string, 0, len(all))
		for _, p := range all {
			names = append(names, p.Name)
		}
		sort.Strings(names)
		res.Text = notFoundText(req.Project, names)
		return res, nil
	}
	res.Projects = found
	res.Text = projectText(res)
	return res, nil
}

// ---- metin üretimi (hepsi sunucuda; model sayı/tarih yazmaz) ----

var statusLabels = map[string]string{
	"planlandi": "planlandı", "devam": "devam ediyor", "beklemede": "beklemede",
	"tamamlandi": "tamamlandı", "iptal": "iptal", "acik": "açık",
	"dusuk": "düşük", "normal": "normal", "yuksek": "yüksek",
}

func label(v string) string {
	if l, ok := statusLabels[v]; ok {
		return l
	}
	return v
}

func clip(s string) string {
	s = NormalizeText(s)
	if utf8.RuneCountInString(s) <= maxReportText {
		return s
	}
	return string([]rune(s)[:maxReportText]) + "…"
}

func dueNote(t TaskInfo) string {
	switch {
	case t.Overdue:
		return fmt.Sprintf("son tarih: %s, %d gün gecikti", t.Due, t.DaysLate)
	case t.Due != "":
		return "son tarih: " + t.Due
	case !t.Open:
		return "" // tamamlanmış/iptal görevde "son tarih yok" demek anlamsız
	}
	return "son tarih yok"
}

func taskLine(i int, t TaskInfo, withProject bool) string {
	parts := []string{}
	if t.Assignee != "" {
		parts = append(parts, "atanan: "+t.Assignee)
	}
	if d := dueNote(t); d != "" {
		parts = append(parts, d)
	}
	if withProject && t.Project != "" {
		parts = append(parts, "proje: "+t.Project)
	}
	parts = append(parts, "durum: "+label(t.Status))
	s := fmt.Sprintf("%d. %s (%s)", i, clip(t.Text), strings.Join(parts, ", "))
	if t.Conflict {
		s += " [çelişkili]"
	}
	return s
}

func overdueText(r *ReportResult) string {
	var b strings.Builder
	fmt.Fprintf(&b, "Gecikmiş görevler (bugün: %s):\n", r.Today)
	if len(r.Tasks) == 0 {
		b.WriteString("Gecikmiş görev yok.")
	}
	for i, t := range r.Tasks {
		b.WriteString(taskLine(i+1, t, true))
		b.WriteByte('\n')
	}
	if len(r.Tasks) > 0 {
		fmt.Fprintf(&b, "\nToplam: %d gecikmiş görev", len(r.Tasks))
	}
	fmt.Fprintf(&b, " (açık görev: %d", r.OpenTasks)
	if r.NoDue > 0 {
		fmt.Fprintf(&b, ", son tarihi olmayan: %d", r.NoDue)
	}
	b.WriteString(")")
	return b.String()
}

func projectHeader(p ProjectInfo) string {
	parts := []string{}
	if p.Status != "" {
		parts = append(parts, "durum: "+label(p.Status))
	}
	if p.Owner != "" {
		parts = append(parts, "sorumlu: "+p.Owner)
	}
	if p.Priority != "" {
		parts = append(parts, "öncelik: "+label(p.Priority))
	}
	h := p.Name
	if len(parts) > 0 {
		h += " (" + strings.Join(parts, ", ") + ")"
	}
	if p.Conflict {
		h += " [çelişkili]"
	}
	return h
}

func endNote(p ProjectInfo) string {
	if !p.EndKnown {
		return ""
	}
	switch {
	case p.Late:
		return fmt.Sprintf("bitiş: %s, %d gün gecikti", p.End, -p.DaysToEnd)
	case p.DaysToEnd < 0:
		return "bitiş: " + p.End
	case p.DaysToEnd == 0:
		return "bitiş: " + p.End + ", bugün"
	}
	return fmt.Sprintf("bitiş: %s, %d gün kaldı", p.End, p.DaysToEnd)
}

func progressNote(p ProjectInfo) string {
	if p.Progress < 0 {
		return "henüz görev yok"
	}
	return fmt.Sprintf("%%%d (%d görevden %d tamamlandı)", p.Progress, p.Total, p.Done)
}

func projectText(r *ReportResult) string {
	var b strings.Builder
	for n, p := range r.Projects {
		if n > 0 {
			b.WriteString("\n\n")
		}
		b.WriteString("Proje: " + projectHeader(p) + "\n")
		if p.Start != "" {
			fmt.Fprintf(&b, "Başlangıç: %s\n", p.Start)
		}
		if e := endNote(p); e != "" {
			b.WriteString(e + "\n")
		}
		fmt.Fprintf(&b, "İlerleme: %s\n", progressNote(p))
		fmt.Fprintf(&b, "Açık görev: %d, gecikmiş görev: %d", p.Open, p.Overdue)
		if p.Cancelled > 0 {
			fmt.Fprintf(&b, ", iptal: %d (ilerlemeye dahil değil)", p.Cancelled)
		}
		b.WriteByte('\n')
		if len(p.Tasks) > 0 {
			b.WriteString("Görevler:\n")
		}
		for i, t := range p.Tasks {
			b.WriteString(taskLine(i+1, t, false))
			b.WriteByte('\n')
		}
	}
	if len(r.Projects) > 1 {
		fmt.Fprintf(&b, "\nAdı eşleşen %d proje bulundu; hangisini kastettiğinizi belirtin.", len(r.Projects))
	}
	return strings.TrimRight(b.String(), "\n")
}

func summaryText(r *ReportResult) string {
	var b strings.Builder
	fmt.Fprintf(&b, "Proje özeti (bugün: %s):\n", r.Today)
	if len(r.Projects) == 0 {
		b.WriteString("Kayıtlı proje bulunamadı.")
	}
	overdue := 0
	for i, p := range r.Projects {
		parts := []string{}
		if p.Status != "" {
			parts = append(parts, label(p.Status))
		}
		parts = append(parts, "ilerleme "+progressNote(p))
		if p.Overdue > 0 {
			parts = append(parts, fmt.Sprintf("%d gecikmiş görev", p.Overdue))
		}
		if e := endNote(p); e != "" {
			parts = append(parts, e)
		}
		line := fmt.Sprintf("%d. %s: %s", i+1, p.Name, strings.Join(parts, "; "))
		if p.Conflict {
			line += " [çelişkili]"
		}
		b.WriteString(line + "\n")
		overdue += p.Overdue
	}
	if len(r.Projects) > 0 {
		fmt.Fprintf(&b, "\nToplam: %d proje, %d gecikmiş görev", len(r.Projects), overdue)
	}
	if n := len(r.Unassigned); n > 0 {
		late := 0
		for _, t := range r.Unassigned {
			if t.Overdue {
				late++
			}
		}
		fmt.Fprintf(&b, "\nProjeye bağlanamayan görev: %d (gecikmiş: %d)", n, late)
	}
	return strings.TrimRight(b.String(), "\n")
}

func notFoundText(asked string, names []string) string {
	asked = clip(asked)
	if len(names) == 0 {
		return fmt.Sprintf("%q adlı proje bulunamadı; kayıtlı proje yok.", asked)
	}
	return fmt.Sprintf("%q adlı proje bulunamadı. Kayıtlı projeler: %s.", asked, strings.Join(names, ", "))
}

// ---- niyet eşlemesi ve Reader girişi ----

// maxProjectAskRunes kullanıcının sorduğu proje adının üst sınırıdır.
const maxProjectAskRunes = 80

// reportAliases modelin rapor adı için yazabileceği yakın biçimlerdir.
var reportAliases = map[string]ReportType{
	"gecikmis_gorev": ReportOverdue, "gecikmis_gorevler": ReportOverdue,
	"geciken_gorev": ReportOverdue, "geciken_gorevler": ReportOverdue, "gecikme": ReportOverdue,
	"proje_durumu": ReportProject, "proje_durum": ReportProject, "proje_ilerleme": ReportProject,
	"proje_ozet": ReportSummary, "proje_ozeti": ReportSummary, "projeler": ReportSummary,
}

// canonicalReport model çıktısını rapor türüne çevirir; bilinmiyorsa "" (geçersiz) döner.
func canonicalReport(s string) ReportType {
	return reportAliases[strings.ReplaceAll(NormalizeKey(s), " ", "_")]
}

// Report hazır raporu hesaplar (bkz. BuildReport). Okuyan kişinin görme sınırı uygulanır.
func (r *Reader) Report(ctx context.Context, req ReportRequest, actor Actor) (*ReportResult, error) {
	return BuildReport(ctx, r.repo, req, actor, r.now(), r.cfg.PageSize)
}
