package opds

import (
	"encoding/xml"
	"fmt"
	"time"
)

// OPDS 1.2 Feed Structures

type Feed struct {
	XMLName      xml.Name `xml:"feed"`
	Xmlns        string   `xml:"xmlns,attr"`
	OpdsNS       string   `xml:"xmlns:opds,attr"`
	DcNS         string   `xml:"xmlns:dc,attr"`
	OpenSearchNS string   `xml:"xmlns:opensearch,attr,omitempty"`
	ID           string   `xml:"id"`
	Title        string   `xml:"title"`
	Updated      string   `xml:"updated"`
	Links        []Link   `xml:"link"`
	TotalResults *int     `xml:"opensearch:totalResults,omitempty"`
	ItemsPerPage *int     `xml:"opensearch:itemsPerPage,omitempty"`
	StartIndex   *int     `xml:"opensearch:startIndex,omitempty"`
	Entries      []Entry  `xml:"entry"`
}

type Entry struct {
	ID         string      `xml:"id"`
	Title      string      `xml:"title"`
	Author     *Author     `xml:"author,omitempty"`
	Updated    string      `xml:"updated"`
	Summary    string      `xml:"summary,omitempty"`
	Links      []Link      `xml:"link"`
	Identifier *Identifier `xml:"dc:identifier,omitempty"`
	Metadata   []Meta      `xml:"meta"`
	Categories []Category  `xml:"category,omitempty"`
}

type Author struct {
	XMLName xml.Name `xml:"author"`
	Name    string   `xml:"name"`
	URI     string   `xml:"uri,omitempty"`
}

type Link struct {
	Href string `xml:"href,attr"`
	Type string `xml:"type,attr"`
	Rel  string `xml:"rel,attr,omitempty"`
}

type Identifier struct {
	XMLName xml.Name `xml:"dc:identifier"`
	ID      string   `xml:"id,attr"`
	Content string   `xml:",chardata"`
}

type Meta struct {
	Property string `xml:"property,attr"`
	Content  string `xml:",chardata"`
}

type Category struct {
	Scheme string `xml:"scheme,attr"`
	Term   string `xml:"term,attr"`
}

// NewFeed creates a new OPDS feed
func NewFeed(feedID, title string) *Feed {
	now := time.Now().Format(time.RFC3339)
	return &Feed{
		Xmlns:        "http://www.w3.org/2005/Atom",
		OpdsNS:       "http://opds-spec.org/2010/",
		DcNS:         "http://purl.org/dc/elements/1.1/",
		OpenSearchNS: "http://a9.com/-/spec/opensearch/1.1/",
		ID:           feedID,
		Title:        title,
		Updated:      now,
		Links:        []Link{},
		Entries:      []Entry{},
	}
}

// SetPagination populates the OpenSearch paging metadata (totalResults,
// itemsPerPage, startIndex). startIndex is 1-based to match the page model.
func (f *Feed) SetPagination(totalResults, itemsPerPage, startIndex int) {
	tr := totalResults
	ipp := itemsPerPage
	si := startIndex
	f.TotalResults = &tr
	f.ItemsPerPage = &ipp
	f.StartIndex = &si
}

// AddLink adds a link to the feed
func (f *Feed) AddLink(href, linkType, rel string) {
	f.Links = append(f.Links, Link{
		Href: href,
		Type: linkType,
		Rel:  rel,
	})
}

// AddEntry adds an entry to the feed
func (f *Feed) AddEntry(entry Entry) {
	f.Entries = append(f.Entries, entry)
}

// NewEntry creates a new OPDS entry
func NewEntry(id, title, creator, updated string) Entry {
	e := Entry{
		ID:       id,
		Title:    title,
		Updated:  updated,
		Links:    []Link{},
		Metadata: []Meta{},
	}
	if creator != "" {
		e.Author = &Author{Name: creator}
	}
	return e
}

// AddAcquisitionLink adds an acquisition link to the entry
func (e *Entry) AddAcquisitionLink(href, linkType string) {
	e.Links = append(e.Links, Link{
		Href: href,
		Type: linkType,
		Rel:  "http://opds-spec.org/acquisition/open-access",
	})
}

// AddAlternateLink adds an alternate link to the entry
func (e *Entry) AddAlternateLink(href, linkType string) {
	e.Links = append(e.Links, Link{
		Href: href,
		Type: linkType,
		Rel:  "alternate",
	})
}

// AddLink adds a generic link to the entry
func (e *Entry) AddLink(href, linkType, rel string) {
	e.Links = append(e.Links, Link{
		Href: href,
		Type: linkType,
		Rel:  rel,
	})
}

// SetIdentifier sets the canonical identifier
func (e *Entry) SetIdentifier(id string) {
	e.Identifier = &Identifier{
		ID:      "bookhoard",
		Content: id,
	}
}

// AddMetadata adds metadata to the entry
func (e *Entry) AddMetadata(property, content string) {
	e.Metadata = append(e.Metadata, Meta{
		Property: property,
		Content:  content,
	})
}

// AddCategory adds a category to the entry
func (e *Entry) AddCategory(scheme, term string) {
	e.Categories = append(e.Categories, Category{
		Scheme: scheme,
		Term:   term,
	})
}

// SetSummary sets the entry summary
func (e *Entry) SetSummary(summary string) {
	e.Summary = summary
}

// GenerateXML generates the OPDS XML
func (f *Feed) GenerateXML() ([]byte, error) {
	output, err := xml.MarshalIndent(f, "", "  ")
	if err != nil {
		return nil, fmt.Errorf("failed to marshal OPDS feed: %w", err)
	}
	return output, nil
}

// GenerateXMLString generates the OPDS XML as a string
func (f *Feed) GenerateXMLString() (string, error) {
	output, err := f.GenerateXML()
	if err != nil {
		return "", err
	}
	return xml.Header + string(output), nil
}

// OpenSearchUrl is a single <Url> element in an OpenSearch description.
type OpenSearchUrl struct {
	XMLName  xml.Name `xml:"Url"`
	Type     string   `xml:"type,attr"`
	Template string   `xml:"template,attr"`
}

// OpenSearchDescription is an OpenSearch description document used by OPDS
// clients (e.g. KOReader) to discover how to perform catalog searches. Clients
// fetch this document at the catalog's rel="search" link, then substitute
// {searchTerms} in the Url template to execute a query.
type OpenSearchDescription struct {
	XMLName        xml.Name      `xml:"OpenSearchDescription"`
	Xmlns          string        `xml:"xmlns,attr"`
	ShortName      string        `xml:"ShortName"`
	Description    string        `xml:"Description"`
	InputEncoding  string        `xml:"InputEncoding"`
	OutputEncoding string        `xml:"OutputEncoding"`
	Url            OpenSearchUrl `xml:"Url"`
}

// NewSearchDescription creates an OpenSearch description document whose Url
// template points clients back to the search results endpoint. The template
// must contain the {searchTerms} placeholder.
func NewSearchDescription(shortName, description, template string) *OpenSearchDescription {
	return &OpenSearchDescription{
		Xmlns:          "http://a9.com/-/spec/opensearch/1.1/",
		ShortName:      shortName,
		Description:    description,
		InputEncoding:  "UTF-8",
		OutputEncoding: "UTF-8",
		Url: OpenSearchUrl{
			Type:     "application/atom+xml;profile=opds-catalog;kind=acquisition",
			Template: template,
		},
	}
}

// GenerateXML generates the OpenSearch description XML
func (d *OpenSearchDescription) GenerateXML() ([]byte, error) {
	output, err := xml.MarshalIndent(d, "", "  ")
	if err != nil {
		return nil, fmt.Errorf("failed to marshal OpenSearch description: %w", err)
	}
	return output, nil
}

// GenerateXMLString generates the OpenSearch description XML as a string
func (d *OpenSearchDescription) GenerateXMLString() (string, error) {
	output, err := d.GenerateXML()
	if err != nil {
		return "", err
	}
	return xml.Header + string(output), nil
}

// NewErrorFeed creates an error feed
func NewErrorFeed(message string) *Feed {
	feed := NewFeed(
		fmt.Sprintf("urn:uuid:error-%d", time.Now().Unix()),
		"Error",
	)
	feed.AddEntry(Entry{
		ID:      "urn:uuid:error",
		Title:   "Error",
		Summary: message,
		Updated: feed.Updated,
	})
	return feed
}
