package service

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

type OpenLibraryProvider struct {
	baseURL    string
	httpClient *http.Client
}

func NewOpenLibraryProvider() *OpenLibraryProvider {
	return &OpenLibraryProvider{
		baseURL: "https://openlibrary.org",
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

type olSearchResponse struct {
	NumFound int     `json:"numFound"`
	Docs     []olDoc `json:"docs"`
}

type olDoc struct {
	Key            string   `json:"key"`
	Title          string   `json:"title"`
	FirstPublishYear int    `json:"first_publish_year"`
	AuthorName     []string `json:"author_name"`
	ISBN           []string `json:"isbn"`
	Publisher      []string `json:"publisher"`
	CoverID        int      `json:"cover_i"`
	Subject        []string `json:"subject"`
}

type olWorkDetail struct {
	Title       string          `json:"title"`
	Description interface{}     `json:"description"`
	Covers      []int           `json:"covers"`
	Authors     []olAuthorRef   `json:"authors"`
}

type olAuthorRef struct {
	Author struct {
		Key string `json:"key"`
	} `json:"author"`
}

type olAuthorDetail struct {
	Name      string      `json:"name"`
	Bio       interface{} `json:"bio"`
	BirthDate string      `json:"birth_date"`
	DeathDate string      `json:"death_date"`
	Photos    []int       `json:"photos"`
}

func (p *OpenLibraryProvider) Name() string {
	return "openlibrary"
}

func (p *OpenLibraryProvider) fetch(ctx context.Context, url string, result interface{}) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}

	resp, err := p.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("fetch open library: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("open library api returned status %d", resp.StatusCode)
	}

	if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
		return fmt.Errorf("decode open library response: %w", err)
	}

	return nil
}

func extractOLDescription(desc interface{}) string {
	switch v := desc.(type) {
	case string:
		return v
	case map[string]interface{}:
		if val, ok := v["value"].(string); ok {
			return val
		}
	}
	return ""
}

func (p *OpenLibraryProvider) Search(ctx context.Context, query string, opts SearchOptions) ([]MetadataSearchResult, error) {
	url := fmt.Sprintf("%s/search.json?q=%s&limit=20", p.baseURL, query)
	if opts.Year != nil {
		url += fmt.Sprintf("&first_publish_year=%d", *opts.Year)
	}

	var resp olSearchResponse
	if err := p.fetch(ctx, url, &resp); err != nil {
		return nil, fmt.Errorf("search open library: %w", err)
	}

	var results []MetadataSearchResult
	for _, doc := range resp.Docs {
		var year *int
		if doc.FirstPublishYear != 0 {
			y := doc.FirstPublishYear
			year = &y
		}

		extIDMap := map[string]string{"openlibrary": doc.Key}
		if len(doc.ISBN) > 0 {
			extIDMap["isbn"] = doc.ISBN[0]
		}
		extIDs, _ := json.Marshal(extIDMap)

		authorName := ""
		if len(doc.AuthorName) > 0 {
			authorName = doc.AuthorName[0]
		}

		results = append(results, MetadataSearchResult{
			ProviderID:    doc.Key,
			Title:         doc.Title,
			Year:          year,
			MediaType:     "book",
			OriginalTitle: authorName,
			ExternalIDs:   extIDs,
		})
	}

	return results, nil
}

func (p *OpenLibraryProvider) GetDetails(ctx context.Context, id string) (*MetadataDetails, error) {
	workURL := fmt.Sprintf("%s%s.json", p.baseURL, id)

	var work olWorkDetail
	if err := p.fetch(ctx, workURL, &work); err != nil {
		return nil, fmt.Errorf("get open library work: %w", err)
	}

	extIDs, _ := json.Marshal(map[string]string{"openlibrary": id})

	description := extractOLDescription(work.Description)

	authorNames := []string{}
	for _, aRef := range work.Authors {
		authorURL := fmt.Sprintf("%s%s.json", p.baseURL, aRef.Author.Key)
		var author olAuthorDetail
		if err := p.fetch(ctx, authorURL, &author); err == nil {
			authorNames = append(authorNames, author.Name)
		}
	}

	metadata, _ := json.Marshal(map[string]interface{}{
		"authors":  authorNames,
		"covers":   work.Covers,
	})

	overview := description

	return &MetadataDetails{
		ProviderID: id,
		Title:      work.Title,
		Overview:   &overview,
		ExternalIDs: extIDs,
		Metadata:   metadata,
	}, nil
}

func (p *OpenLibraryProvider) GetImages(ctx context.Context, id string) ([]ImageResult, error) {
	workURL := fmt.Sprintf("%s%s.json", p.baseURL, id)

	var work olWorkDetail
	if err := p.fetch(ctx, workURL, &work); err != nil {
		return nil, nil
	}

	if len(work.Covers) == 0 {
		return nil, nil
	}

	coverID := work.Covers[0]

	return []ImageResult{{
		URL:  fmt.Sprintf("https://covers.openlibrary.org/b/id/%d-L.jpg", coverID),
		Type: "cover",
	}}, nil
}
