package models

import (
	"errors"
	"fmt"
	"time"
	"unicode/utf8"

	"github.com/bluesky-social/indigo/atproto/syntax"
	lexutil "github.com/bluesky-social/indigo/lex/util"
	"tangled.org/core/api/tangled"
)

type String struct {
	Did  syntax.DID
	Rkey syntax.RecordKey
	Cid  *syntax.CID

	// String_File will remain, and we will still use it.
	// We just use `FileContent` when fetching the file.

	// after that, change lexicon, start migrating to blobs
	// when string is migrated, clear FileName and FileContent.
	// when they are cleared, fetch the blob on page load.

	Title       *string
	Description *string
	Files       []String_File
	Created     time.Time
	Edited      *time.Time

	// legacy string data
	FileName    string
	FileContent string

	// optionally, populate this when querying for reverse mappings
	Stats *StringStats
}

// String_File is [tangled.String_File] with optional prefetched & decompressed text blob content
type String_File struct {
	Name    string
	Content lexutil.LexBlob
	Gzip    *String_GzipInfo
}
type String_GzipInfo struct {
	tangled.String_File_Gzip
	// Optional uncompressed content.
	// Populated when the content is first requested.
	Content string
}

func (s *String) AtUri() syntax.ATURI {
	return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", s.Did, tangled.StringNSID, s.Rkey))
}

func (s *String) AsRecord() *tangled.String {
	var files []*tangled.String_File
	for _, f := range s.Files {
		var gzip *tangled.String_File_Gzip
		if f.Gzip != nil {
			gzip = &tangled.String_File_Gzip{
				RealSize: f.Gzip.RealSize,
				RealMime: f.Gzip.RealMime,
			}
		}
		files = append(files, &tangled.String_File{
			Name:    f.Name,
			Content: &f.Content,
			Gzip:    gzip,
		})
	}
	return &tangled.String{
		Title:       s.Title,
		Description: s.Description,
		Files:       files,
		CreatedAt:   s.Created.Format(time.RFC3339),
	}
}

func (s *String) Validate() error {
	var err error
	if s.FileName == "" && len(s.Files) == 0 {
		err = errors.Join(err, fmt.Errorf("string should have more than one files"))
	}
	// legacy record check
	if utf8.RuneCountInString(s.FileName) > 140 {
		err = errors.Join(err, fmt.Errorf("filename too long"))
	}
	for i, file := range s.Files {
		if utf8.RuneCountInString(file.Name) > 140 {
			err = errors.Join(err, fmt.Errorf("filename too long at files[%d]", i))
		}
	}
	if s.Title != nil {
		if utf8.RuneCountInString(*s.Title) > 140 {
			err = errors.Join(err, fmt.Errorf("title too long"))
		}
	}
	if s.Description != nil {
		if utf8.RuneCountInString(*s.Description) > 280 {
			err = errors.Join(err, fmt.Errorf("description too long"))
		}
	}
	return err
}

func (s String) RenderTitle() string {
	if s.Title != nil {
		return *s.Title
	}
	if len(s.Files) > 0 {
		return s.Files[0].Name
	}
	return s.FileName
}

// FileByName returns first item in files with given filename
func (s *String) FileByName(name string) (String_File, bool) {
	for _, file := range s.Files {
		if file.Name == name {
			return file, true
		}
	}
	return String_File{}, false
}

func (s String) IsLegacySingleFile() bool {
	return len(s.Files) == 0
}

// StringFromRecord creates [String] from [tangled.String].
// NOTE: This won't prefetch blobs
func StringFromRecord(did syntax.DID, rkey syntax.RecordKey, cid syntax.CID, record tangled.String) (String, error) {
	created, err := time.Parse(time.RFC3339, record.CreatedAt)
	if err != nil {
		return String{}, fmt.Errorf("invalid createdAt: %w", err)
	}
	var files []String_File
	for _, f := range record.Files {
		var gzip *String_GzipInfo
		if f.Gzip != nil {
			gzip = &String_GzipInfo{String_File_Gzip: *f.Gzip}
		}
		files = append(files, String_File{
			Name:    f.Name,
			Content: *f.Content,
			Gzip:    gzip,
		})
	}
	return String{
		Did:         did,
		Rkey:        rkey,
		Cid:         &cid,
		Title:       record.Title,
		Description: record.Description,
		Files:       files,
		Created:     created,
		FileName:    stringPtr(record.Filename),
		FileContent: stringPtr(record.Contents),
	}, nil
}

type StringStats struct {
	StarCount int
	// CommentCount int
}

type StringFileStats struct {
	LineCount int
	ByteCount int
}

func stringPtr(s *string) string {
	if s == nil {
		return ""
	}
	return *s
}
