package knowledge

import (
	"bytes"
	"errors"
	"fmt"
	"html"
	"path/filepath"
	"regexp"
	"strings"
	"unicode"
	"unicode/utf8"
)

// ErrUnsupportedFormat dosya biçimi okunamıyorsa döner (PDF, Office, ikili dosyalar).
var ErrUnsupportedFormat = errors.New("knowledge: desteklenmeyen dosya biçimi")

// ErrEmptyText ayrıştırma sonrası okunabilir metin kalmadıysa döner.
var ErrEmptyText = errors.New("knowledge: belgede okunabilir metin yok")

// Parse yüklenen içeriği düz metne çevirir.
//
// Desteklenen: düz metin ve metin tabanlı biçimler (txt, md, csv, json, log, kaynak kod, ...),
// HTML. PDF/Office dosyaları bilerek REDDEDİLİR (sessizce çöp metin üretmek yerine net hata):
// bunlar ek bağımlılık gerektirir ve metne çevrilerek yüklenebilir.
//
// Kodlama: geçerli UTF-8 olduğu gibi okunur; değilse Windows-1254 (Türkçe) varsayılır —
// Türkçe .txt dosyalarının en yaygın eski kodlaması.
func Parse(filename, contentType string, data []byte) (string, error) {
	if len(data) == 0 {
		return "", ErrEmptyText
	}
	if why := unsupportedReason(filename, contentType, data); why != "" {
		return "", fmt.Errorf("%w: %s", ErrUnsupportedFormat, why)
	}
	if bytes.IndexByte(data, 0) >= 0 {
		return "", fmt.Errorf("%w: ikili içerik (NUL baytı)", ErrUnsupportedFormat)
	}

	var text string
	if utf8.Valid(data) {
		text = string(data)
	} else {
		text = decodeWindows1254(data)
	}

	if isHTML(filename, contentType, text) {
		text = stripHTML(text)
	}
	text = NormalizeText(text)
	if text == "" {
		return "", ErrEmptyText
	}
	return text, nil
}

// unsupportedReason, desteklenmeyen biçim için kullanıcıya gösterilecek nedeni döner; destekliyse "".
func unsupportedReason(filename, contentType string, data []byte) string {
	ext := strings.ToLower(filepath.Ext(filename))
	switch ext {
	case ".pdf":
		return "PDF henüz desteklenmiyor; metne çevirip yükleyin"
	case ".docx", ".doc", ".xlsx", ".xls", ".pptx", ".ppt", ".odt", ".rtf":
		return "Office belgeleri henüz desteklenmiyor; metne çevirip yükleyin"
	case ".zip", ".7z", ".rar", ".gz", ".tar", ".exe", ".dll", ".bin":
		return "arşiv/ikili dosya"
	case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".mp3", ".wav", ".mp4":
		return "görsel/ses dosyası"
	}
	ct := strings.ToLower(strings.TrimSpace(strings.SplitN(contentType, ";", 2)[0]))
	if ct == "application/pdf" {
		return "PDF henüz desteklenmiyor; metne çevirip yükleyin"
	}
	if strings.HasPrefix(ct, "image/") || strings.HasPrefix(ct, "audio/") || strings.HasPrefix(ct, "video/") {
		return "görsel/ses/video dosyası"
	}
	// Uzantı/İçerik türü yanıltabilir: ilk baytlara (magic) da bakılır.
	switch {
	case bytes.HasPrefix(data, []byte("%PDF-")):
		return "PDF henüz desteklenmiyor; metne çevirip yükleyin"
	case bytes.HasPrefix(data, []byte("PK\x03\x04")):
		return "arşiv ya da Office belgesi"
	}
	return ""
}

func isHTML(filename, contentType, text string) bool {
	switch strings.ToLower(filepath.Ext(filename)) {
	case ".html", ".htm", ".xhtml":
		return true
	}
	if strings.HasPrefix(strings.ToLower(strings.TrimSpace(contentType)), "text/html") {
		return true
	}
	return false
}

var (
	reHTMLComment = regexp.MustCompile(`(?s)<!--.*?-->`)
	// Go RE2 geri başvuru (\1) desteklemez; kapanış etiketi her tür için ayrı yazılır.
	reHTMLDrop = []*regexp.Regexp{
		regexp.MustCompile(`(?is)<script\b.*?</script\s*>`),
		regexp.MustCompile(`(?is)<style\b.*?</style\s*>`),
		regexp.MustCompile(`(?is)<noscript\b.*?</noscript\s*>`),
		regexp.MustCompile(`(?is)<template\b.*?</template\s*>`),
		regexp.MustCompile(`(?is)<svg\b.*?</svg\s*>`),
		regexp.MustCompile(`(?is)<head\b.*?</head\s*>`),
	}
	reHTMLBlock = regexp.MustCompile(`(?i)</?(?:p|div|br|li|ul|ol|h[1-6]|tr|table|section|article|header|footer|blockquote|pre|hr)\b[^>]*>`)
	reHTMLTag   = regexp.MustCompile(`<[^>]*>`)
)

// stripHTML etiketleri atar; blok etiketleri satır sonuna, varlıkları (&amp;) karakterlere çevirir.
func stripHTML(s string) string {
	s = reHTMLComment.ReplaceAllString(s, "")
	for _, re := range reHTMLDrop {
		s = re.ReplaceAllString(s, "")
	}
	s = reHTMLBlock.ReplaceAllString(s, "\n")
	s = reHTMLTag.ReplaceAllString(s, "")
	return html.UnescapeString(s)
}

var reManyNewlines = regexp.MustCompile(`\n{3,}`)

// NormalizeText metni arama/parçalama için tutarlı hâle getirir: BOM ve kontrol karakterleri
// atılır, satır sonları "\n" olur, satır sonu boşlukları ve 3+ boş satır sadeleşir.
// Aynı içerik farklı satır sonuyla yüklense de aynı özet (hash) çıkar.
func NormalizeText(s string) string {
	s = strings.TrimPrefix(s, "\uFEFF")
	s = strings.ReplaceAll(s, "\r\n", "\n")
	s = strings.ReplaceAll(s, "\r", "\n")
	s = strings.Map(func(r rune) rune {
		switch {
		case r == '\n' || r == '\t':
			return r
		case r == '\u00A0': // bölünmez boşluk
			return ' '
		case unicode.IsControl(r) || r == '\uFFFD':
			return -1
		}
		return r
	}, s)
	lines := strings.Split(s, "\n")
	for i, l := range lines {
		lines[i] = strings.TrimRight(l, " \t")
	}
	s = strings.Join(lines, "\n")
	s = reManyNewlines.ReplaceAllString(s, "\n\n")
	return strings.TrimSpace(s)
}

// cp1254High Windows-1254'ün 0x80–0x9F aralığı (yazım işaretleri). 0 = tanımsız bayt.
var cp1254High = [32]rune{
	0x20AC, 0, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
	0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0, 0, 0,
	0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
	0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0, 0, 0x0178,
}

// decodeWindows1254 baytları Windows-1254 (Türkçe) olarak UTF-8'e çevirir.
// 0xA0 ve üstü ISO-8859-9 ile aynıdır; Latin-1'den yalnızca altı Türkçe harf farklıdır.
func decodeWindows1254(b []byte) string {
	var sb strings.Builder
	sb.Grow(len(b) + len(b)/4)
	for _, c := range b {
		switch {
		case c < 0x80:
			sb.WriteByte(c)
		case c < 0xA0:
			if r := cp1254High[c-0x80]; r != 0 {
				sb.WriteRune(r)
			} else {
				sb.WriteByte(' ')
			}
		default:
			switch c {
			case 0xD0:
				sb.WriteRune('Ğ')
			case 0xDD:
				sb.WriteRune('İ')
			case 0xDE:
				sb.WriteRune('Ş')
			case 0xF0:
				sb.WriteRune('ğ')
			case 0xFD:
				sb.WriteRune('ı')
			case 0xFE:
				sb.WriteRune('ş')
			default:
				sb.WriteRune(rune(c))
			}
		}
	}
	return sb.String()
}
