package lib

import (
	"crypto/md5"
	"errors"
	"fmt"
	"path/filepath"
	"sort"
	"time"

	"gitlab.com/romalor/pdflib/pkg/components/attachment"
	"gitlab.com/romalor/pdflib/pkg/core"
	"gitlab.com/romalor/pdflib/pkg/pdf"
)

// AddAttachment embeds data as a file attachment.
// The att.Name field specifies the attachment name.
func AddAttachment(d *pdf.Document, att attachment.Attachment, data []byte) error {
	if !d.IsWritable() || d.Writer() == nil {
		return pdf.ErrNotWritable
	}

	if att.Name == "" {
		return errors.New("attachment name cannot be empty")
	}

	// Apply defaults
	modDate := att.ModDate
	if modDate.IsZero() {
		modDate = time.Now()
	}
	relationship := att.Relationship
	if relationship == "" {
		relationship = "Unspecified"
	}

	// Guess MIME type if not provided
	mimeType := att.MIMEType
	if mimeType == "" {
		mimeType = guessMIMEType(att.Name)
	}

	// Calculate checksum
	checksum := md5.Sum(data)

	// Create embedded file stream
	efStreamDict := core.NewDictionary()
	efStreamDict.Set(core.Name("Type"), core.Name("EmbeddedFile"))
	if mimeType != "" {
		efStreamDict.Set(core.Name("Subtype"), core.Name(mimeType))
	}

	// Create Params dictionary
	params := core.NewDictionary()
	params.Set(core.Name("Size"), core.Integer(len(data)))
	params.Set(core.Name("ModDate"), core.String(Format(modDate)))
	params.Set(core.Name("CheckSum"), core.HexString(checksum[:]))
	efStreamDict.Set(core.Name("Params"), params)

	// Create the stream (compression is handled by the writer)
	var efStream *core.Stream
	if !att.NoCompress {
		efStreamDict.Set(core.Name("Filter"), core.Name("FlateDecode"))
	}
	efStream = core.NewStreamFromBytes(efStreamDict, data)
	efStreamRef := d.Writer().Add(efStream)

	// Create EF (embedded files) dictionary
	efDict := core.NewDictionary()
	efDict.Set(core.Name("F"), efStreamRef)
	efDict.Set(core.Name("UF"), efStreamRef) // Unicode-compatible version

	// Create file specification dictionary
	fileSpec := core.NewDictionary()
	fileSpec.Set(core.Name("Type"), core.Name("Filespec"))
	fileSpec.Set(core.Name("F"), core.String(att.Name))
	fileSpec.Set(core.Name("UF"), core.String(att.Name)) // Unicode filename
	fileSpec.Set(core.Name("EF"), efDict)

	if att.Description != "" {
		fileSpec.Set(core.Name("Desc"), core.String(att.Description))
	}

	if relationship != "Unspecified" {
		fileSpec.Set(core.Name("AFRelationship"), core.Name(relationship))
	}

	fileSpecRef := d.Writer().Add(fileSpec)

	// Update the EmbeddedFiles name tree
	if err := addToEmbeddedFilesNameTree(d, att.Name, fileSpecRef); err != nil {
		return fmt.Errorf("adding to embedded files tree: %w", err)
	}

	return nil
}

// addToEmbeddedFilesNameTree adds an entry to the EmbeddedFiles name tree.
func addToEmbeddedFilesNameTree(d *pdf.Document, name string, fileSpecRef core.Reference) error {
	catalog, catalogRef, err := d.Resolver().Catalog()
	if err != nil {
		return err
	}

	// Try to get existing Names dictionary
	var namesDict *core.Dictionary
	var namesRef core.Reference
	var namesIsRef bool

	if namesObj, ok := catalog.Get(core.Name("Names")); ok {
		if ref, isRef := namesObj.(core.Reference); isRef {
			namesRef = ref
			namesIsRef = true
		}
		namesDict, err = core.ResolveDict(d.Resolver(), namesObj)
		if err != nil {
			return fmt.Errorf("resolving Names dictionary: %w", err)
		}
		// Clone the dictionary
		namesDict = namesDict.CloneDict()
	} else {
		// Create new Names dictionary
		namesDict = core.NewDictionary()
	}

	// Try to get existing EmbeddedFiles name tree
	var efDict *core.Dictionary
	var efRef core.Reference
	var efIsRef bool

	if efObj, ok := namesDict.Get(core.Name("EmbeddedFiles")); ok {
		if ref, isRef := efObj.(core.Reference); isRef {
			efRef = ref
			efIsRef = true
		}
		efDict, err = core.ResolveDict(d.Resolver(), efObj)
		if err != nil {
			return fmt.Errorf("resolving EmbeddedFiles dictionary: %w", err)
		}
	}

	// Build new name tree with the added entry
	var newNamesArray core.Array

	if efDict != nil {
		// Get existing entries
		entries, err := collectNameTreeEntries(d, efDict)
		if err != nil {
			return fmt.Errorf("collecting existing entries: %w", err)
		}

		// Add new entry
		entries = append(entries, nameTreeEntry{name: name, value: fileSpecRef})

		// Sort by name
		sort.Slice(entries, func(i, j int) bool {
			return entries[i].name < entries[j].name
		})

		// Build names array
		newNamesArray = make(core.Array, 0, len(entries)*2)
		for _, entry := range entries {
			newNamesArray = append(newNamesArray, core.String(entry.name), entry.value)
		}
	} else {
		// Create new names array with single entry
		newNamesArray = core.Array{core.String(name), fileSpecRef}
	}

	// Create new EmbeddedFiles dictionary
	newEFDict := core.NewDictionary()
	newEFDict.Set(core.Name("Names"), newNamesArray)

	// Update or create EmbeddedFiles entry
	if efIsRef {
		if err := d.Writer().Modify(efRef, newEFDict); err != nil {
			return fmt.Errorf("modifying EmbeddedFiles: %w", err)
		}
	} else {
		namesDict.Set(core.Name("EmbeddedFiles"), newEFDict)
	}

	// Update or create Names dictionary
	if namesIsRef {
		if !efIsRef {
			// We added a new EmbeddedFiles entry, need to update Names
			if err := d.Writer().Modify(namesRef, namesDict); err != nil {
				return fmt.Errorf("modifying Names: %w", err)
			}
		}
	} else {
		// Update catalog
		newCatalog := catalog.CloneDict()
		newCatalog.Set(core.Name("Names"), namesDict)

		// Extend AF array
		afArr := core.Array{fileSpecRef}
		newCatalog.Set(core.Name("AF"), afArr)

		if err := d.Writer().Modify(catalogRef, newCatalog); err != nil {
			return fmt.Errorf("modifying catalog: %w", err)
		}
	}

	return nil
}

// nameTreeEntry represents a name-value pair from a name tree.
type nameTreeEntry struct {
	name  string
	value core.Object
}

// collectNameTreeEntries collects all entries from a name tree.
func collectNameTreeEntries(d *pdf.Document, dict *core.Dictionary) ([]nameTreeEntry, error) {
	var entries []nameTreeEntry

	// Check for Names array (leaf node)
	if namesObj, ok := dict.Get(core.Name("Names")); ok {
		namesArray, err := core.ResolveArray(d.Resolver(), namesObj)
		if err != nil {
			return nil, err
		}
		// Names array has pairs: [name1, value1, name2, value2, ...]
		for i := 0; i+1 < len(namesArray); i += 2 {
			var name string
			switch n := namesArray[i].(type) {
			case core.String:
				name = string(n)
			case core.HexString:
				name = string(n)
			default:
				continue
			}
			entries = append(entries, nameTreeEntry{name: name, value: namesArray[i+1]})
		}
	}

	// Check for Kids array (intermediate node)
	if kidsObj, ok := dict.Get(core.Name("Kids")); ok {
		kidsArray, err := core.ResolveArray(d.Resolver(), kidsObj)
		if err != nil {
			return nil, err
		}
		for _, kid := range kidsArray {
			kidDict, err := core.ResolveDict(d.Resolver(), kid)
			if err != nil {
				continue
			}
			kidEntries, err := collectNameTreeEntries(d, kidDict)
			if err != nil {
				continue
			}
			entries = append(entries, kidEntries...)
		}
	}

	return entries, nil
}

// guessMIMEType guesses the MIME type from a filename.
func guessMIMEType(filename string) string {
	ext := filepath.Ext(filename)
	if ext == "" {
		return "application/octet-stream"
	}

	// Common MIME types
	mimeTypes := map[string]string{
		".pdf":  "application/pdf",
		".txt":  "text/plain",
		".html": "text/html",
		".htm":  "text/html",
		".xml":  "application/xml",
		".json": "application/json",
		".csv":  "text/csv",
		".png":  "image/png",
		".jpg":  "image/jpeg",
		".jpeg": "image/jpeg",
		".gif":  "image/gif",
		".svg":  "image/svg+xml",
		".mp3":  "audio/mpeg",
		".mp4":  "video/mp4",
		".zip":  "application/zip",
		".doc":  "application/msword",
		".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
		".xls":  "application/vnd.ms-excel",
		".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
	}

	if mime, ok := mimeTypes[ext]; ok {
		return mime
	}
	return "application/octet-stream"
}

// FormatPDFDate formats a time.Time as a PDF date string.
// The format is: D:YYYYMMDDHHmmSSOHH'mm'
// where O is the timezone indicator (+, or -) and HH'mm' is the offset.
func Format(t time.Time) string {
	// Get timezone offset
	_, offset := t.Zone()
	offsetHours := offset / 3600
	offsetMins := (offset % 3600) / 60

	var sign byte = '+'
	if offsetHours < 0 {
		sign = '-'
		offsetHours = -offsetHours
		offsetMins = -offsetMins
	}

	return fmt.Sprintf("D:%04d%02d%02d%02d%02d%02d%c%02d'%02d'",
		t.Year(), t.Month(), t.Day(),
		t.Hour(), t.Minute(), t.Second(),
		sign, offsetHours, offsetMins)
}
