package export

// chatgpt.go — v0.34.0 CS4
//
// Parses ChatGPT conversation exports (conversations.json) and converts
// them to Switchboard channel/message models.

import (
	"encoding/json"
	"fmt"
	"io"
	"sort"
	"time"

	"chat-switchboard/models"
	"chat-switchboard/store"
)

// ── ChatGPT format types ─────────────────────

// ChatGPTExport is the top-level conversations.json structure.
type ChatGPTExport []ChatGPTConversation

// ChatGPTConversation represents one conversation in the ChatGPT export.
type ChatGPTConversation struct {
	Title          string                       `json:"title"`
	CreateTime     float64                      `json:"create_time"`
	UpdateTime     float64                      `json:"update_time"`
	Mapping        map[string]ChatGPTMappingNode `json:"mapping"`
	ConversationID string                       `json:"conversation_id"`
}

// ChatGPTMappingNode is a node in the conversation DAG.
type ChatGPTMappingNode struct {
	ID       string          `json:"id"`
	Parent   *string         `json:"parent"`
	Children []string        `json:"children"`
	Message  *ChatGPTMessage `json:"message"`
}

// ChatGPTMessage is a message within a mapping node.
type ChatGPTMessage struct {
	ID         string          `json:"id"`
	Author     ChatGPTAuthor   `json:"author"`
	CreateTime *float64        `json:"create_time"`
	Content    ChatGPTContent  `json:"content"`
	Metadata   json.RawMessage `json:"metadata"`
}

// ChatGPTAuthor identifies the message sender.
type ChatGPTAuthor struct {
	Role string `json:"role"`
}

// ChatGPTContent holds the message content.
type ChatGPTContent struct {
	ContentType string        `json:"content_type"`
	Parts       []interface{} `json:"parts"`
}

// ── Parsing ──────────────────────────────────

// ParseChatGPTExport reads and parses a conversations.json file.
func ParseChatGPTExport(r io.Reader) (ChatGPTExport, error) {
	var out ChatGPTExport
	if err := json.NewDecoder(r).Decode(&out); err != nil {
		return nil, fmt.Errorf("chatgpt: parse error: %w", err)
	}
	return out, nil
}

// ── Conversion ───────────────────────────────

// ChatGPTImportResult holds converted channels and messages.
type ChatGPTImportResult struct {
	Channels []models.Channel
	Messages []models.Message
}

// ConvertChatGPTExport converts ChatGPT conversations to Switchboard models.
func ConvertChatGPTExport(convs ChatGPTExport, userID string) *ChatGPTImportResult {
	result := &ChatGPTImportResult{}

	for _, conv := range convs {
		ch, msgs := convertConversation(conv, userID)
		if ch != nil {
			result.Channels = append(result.Channels, *ch)
			result.Messages = append(result.Messages, msgs...)
		}
	}

	return result
}

func convertConversation(conv ChatGPTConversation, userID string) (*models.Channel, []models.Message) {
	channelID := store.NewID()

	title := conv.Title
	if title == "" {
		title = "Imported Conversation"
	}

	createdAt := floatToTime(conv.CreateTime)
	updatedAt := floatToTime(conv.UpdateTime)
	if updatedAt.IsZero() {
		updatedAt = createdAt
	}

	ch := &models.Channel{
		BaseModel: models.BaseModel{
			ID:        channelID,
			CreatedAt: createdAt,
			UpdatedAt: updatedAt,
		},
		UserID: userID,
		Title:  title,
		Type:   "direct",
	}

	// Walk the DAG depth-first to produce ordered messages
	msgs := walkDAG(conv.Mapping, channelID, userID)

	return ch, msgs
}

func walkDAG(mapping map[string]ChatGPTMappingNode, channelID, userID string) []models.Message {
	// Find root nodes (no parent or parent not in mapping)
	var roots []string
	for id, node := range mapping {
		if node.Parent == nil {
			roots = append(roots, id)
		} else if _, ok := mapping[*node.Parent]; !ok {
			roots = append(roots, id)
		}
	}

	var msgs []models.Message
	idMap := make(map[string]string) // chatgpt node ID → switchboard message ID

	var walk func(nodeID string, parentMsgID *string, siblingIdx int)
	walk = func(nodeID string, parentMsgID *string, siblingIdx int) {
		node, ok := mapping[nodeID]
		if !ok {
			return
		}

		if node.Message != nil && node.Message.Content.ContentType != "" {
			content := extractParts(node.Message.Content.Parts)
			if content != "" {
				msgID := store.NewID()
				idMap[nodeID] = msgID

				role := mapRole(node.Message.Author.Role)
				createdAt := time.Now().UTC()
				if node.Message.CreateTime != nil {
					createdAt = floatToTime(*node.Message.CreateTime)
				}

				msg := models.Message{
					BaseModel: models.BaseModel{
						ID:        msgID,
						CreatedAt: createdAt,
						UpdatedAt: createdAt,
					},
					ChannelID:       channelID,
					Role:            role,
					Content:         content,
					ParentID:        parentMsgID,
					SiblingIndex:    siblingIdx,
					ParticipantType: "user",
					ParticipantID:   userID,
				}

				if role == "assistant" || role == "system" || role == "tool" {
					msg.ParticipantType = role
					msg.ParticipantID = ""
				}

				msgs = append(msgs, msg)
				parentMsgID = &msgID
			}
		}

		// Sort children for deterministic order
		children := make([]string, len(node.Children))
		copy(children, node.Children)
		sort.Strings(children)

		for i, childID := range children {
			walk(childID, parentMsgID, i)
		}
	}

	sort.Strings(roots)
	for _, root := range roots {
		walk(root, nil, 0)
	}

	return msgs
}

func mapRole(role string) string {
	switch role {
	case "user":
		return "user"
	case "assistant":
		return "assistant"
	case "system":
		return "system"
	case "tool":
		return "tool"
	default:
		return "user"
	}
}

func extractParts(parts []interface{}) string {
	var texts []string
	for _, part := range parts {
		switch v := part.(type) {
		case string:
			if v != "" {
				texts = append(texts, v)
			}
		}
	}
	if len(texts) == 0 {
		return ""
	}
	result := texts[0]
	for i := 1; i < len(texts); i++ {
		result += "\n" + texts[i]
	}
	return result
}

func floatToTime(f float64) time.Time {
	if f <= 0 {
		return time.Time{}
	}
	sec := int64(f)
	nsec := int64((f - float64(sec)) * 1e9)
	return time.Unix(sec, nsec).UTC()
}
