package handlers

// import_data.go — v0.34.0 CS1
//
// Data import endpoint: user data import from .switchboard archives.
// Reads the archive, validates manifest, and imports entities in
// FK-dependency order with UUID dedup (skip if ID exists).

import (
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/gin-gonic/gin"

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

// DataImportHandler serves data import endpoints.
type DataImportHandler struct {
	stores   store.Stores
	objStore storage.ObjectStore
}

// NewDataImportHandler creates a new handler for data import operations.
func NewDataImportHandler(s store.Stores, obj storage.ObjectStore) *DataImportHandler {
	return &DataImportHandler{stores: s, objStore: obj}
}

// ImportMyData imports user data from a .switchboard zip archive.
// POST /api/v1/import/me
func (h *DataImportHandler) ImportMyData(c *gin.Context) {
	ctx := c.Request.Context()
	userID := c.GetString("user_id")

	// ── Receive upload ──
	file, header, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "missing file field"})
		return
	}
	defer file.Close()

	// Validate extension
	if !strings.HasSuffix(header.Filename, export.ExportExtension) && !strings.HasSuffix(header.Filename, ".zip") {
		c.JSON(http.StatusBadRequest, gin.H{"error": "file must be a .switchboard or .zip archive"})
		return
	}

	// Size check: use MaxExportArchiveSize
	if header.Size > export.MaxExportArchiveSize {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("archive too large (max %d MB)", export.MaxExportArchiveSize/(1024*1024)),
		})
		return
	}

	// Save to temp file (zip.OpenReader needs a file path)
	tmp, err := os.CreateTemp("", "switchboard-import-*.zip")
	if err != nil {
		slog.Error("import: create temp file", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create temp file"})
		return
	}
	defer os.Remove(tmp.Name())
	defer tmp.Close()

	if _, err := io.Copy(tmp, file); err != nil {
		slog.Error("import: save temp file", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save upload"})
		return
	}
	tmp.Close()

	// ── Open archive ──
	ar, err := export.OpenArchive(tmp.Name())
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid archive: " + err.Error()})
		return
	}
	defer ar.Close()

	manifest, err := ar.ReadManifest()
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid manifest: " + err.Error()})
		return
	}

	if manifest.FormatVersion > export.FormatVersion {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("unsupported format version %d (max %d)", manifest.FormatVersion, export.FormatVersion),
		})
		return
	}

	// ── Read entities from archive ──
	imported := make(map[string]int)
	skipped := make(map[string]int)
	var errors []string

	importEntity := func(name string, fn func() (int, int, error)) {
		imp, skip, err := fn()
		if err != nil {
			errors = append(errors, fmt.Sprintf("%s: %v", name, err))
			slog.Error("import: entity error", "entity", name, "error", err)
		}
		if imp > 0 {
			imported[name] = imp
		}
		if skip > 0 {
			skipped[name] = skip
		}
	}

	// Helper to remap user_id on entities for cross-instance import
	remapUserID := manifest.Scope != nil && manifest.Scope.UserID != "" && manifest.Scope.UserID != userID
	sourceUserID := ""
	if manifest.Scope != nil {
		sourceUserID = manifest.Scope.UserID
	}

	// 1. Folders
	var folders []models.Folder
	if err := ar.ReadEntityJSON("folders", &folders); err == nil && len(folders) > 0 {
		if remapUserID {
			for i := range folders {
				if folders[i].UserID == sourceUserID {
					folders[i].UserID = userID
				}
			}
		}
		importEntity("folders", func() (int, int, error) {
			return h.stores.Export.ImportFolders(ctx, folders)
		})
	}

	// 2. Projects
	var projects []models.Project
	if err := ar.ReadEntityJSON("projects", &projects); err == nil && len(projects) > 0 {
		if remapUserID {
			for i := range projects {
				if projects[i].OwnerID == sourceUserID {
					projects[i].OwnerID = userID
				}
			}
		}
		importEntity("projects", func() (int, int, error) {
			return h.stores.Export.ImportProjects(ctx, projects)
		})
	}

	// 3. Workspaces
	var workspaces []models.Workspace
	if err := ar.ReadEntityJSON("workspaces", &workspaces); err == nil && len(workspaces) > 0 {
		if remapUserID {
			for i := range workspaces {
				if workspaces[i].OwnerID == sourceUserID {
					workspaces[i].OwnerID = userID
				}
			}
		}
		importEntity("workspaces", func() (int, int, error) {
			return h.stores.Export.ImportWorkspaces(ctx, workspaces)
		})
	}

	// 4. Channels
	var channels []models.Channel
	if err := ar.ReadEntityJSON("channels", &channels); err == nil && len(channels) > 0 {
		if remapUserID {
			for i := range channels {
				if channels[i].UserID == sourceUserID {
					channels[i].UserID = userID
				}
			}
		}
		importEntity("channels", func() (int, int, error) {
			return h.stores.Export.ImportChannels(ctx, channels)
		})
	}

	// 5. Channel participants
	var participants []models.ChannelParticipant
	if err := ar.ReadEntityJSON("channel_participants", &participants); err == nil && len(participants) > 0 {
		if remapUserID {
			for i := range participants {
				if participants[i].ParticipantType == "user" && participants[i].ParticipantID == sourceUserID {
					participants[i].ParticipantID = userID
				}
			}
		}
		importEntity("channel_participants", func() (int, int, error) {
			return h.stores.Export.ImportChannelParticipants(ctx, participants)
		})
	}

	// 5b. Channel models
	var channelModels []models.ChannelModel
	if err := ar.ReadEntityJSON("channel_models", &channelModels); err == nil && len(channelModels) > 0 {
		importEntity("channel_models", func() (int, int, error) {
			return h.stores.Export.ImportChannelModels(ctx, channelModels)
		})
	}

	// 5c. Channel cursors
	var cursors []models.ChannelCursor
	if err := ar.ReadEntityJSON("channel_cursors", &cursors); err == nil && len(cursors) > 0 {
		if remapUserID {
			for i := range cursors {
				if cursors[i].UserID == sourceUserID {
					cursors[i].UserID = userID
				}
			}
		}
		importEntity("channel_cursors", func() (int, int, error) {
			return h.stores.Export.ImportChannelCursors(ctx, cursors)
		})
	}

	// 6. Messages (ordered by created_at ASC for parent_id integrity)
	var messages []models.Message
	if err := ar.ReadEntityJSON("messages", &messages); err == nil && len(messages) > 0 {
		if remapUserID {
			for i := range messages {
				if messages[i].ParticipantType == "user" && messages[i].ParticipantID == sourceUserID {
					messages[i].ParticipantID = userID
				}
			}
		}
		importEntity("messages", func() (int, int, error) {
			return h.stores.Export.ImportMessages(ctx, messages)
		})
	}

	// 7. Notes
	var notes []models.Note
	if err := ar.ReadEntityJSON("notes", &notes); err == nil && len(notes) > 0 {
		if remapUserID {
			for i := range notes {
				if notes[i].UserID == sourceUserID {
					notes[i].UserID = userID
				}
			}
		}
		importEntity("notes", func() (int, int, error) {
			return h.stores.Export.ImportNotes(ctx, notes)
		})
	}

	// 8. Note links
	var noteLinks []models.ExportNoteLink
	if err := ar.ReadEntityJSON("note_links", &noteLinks); err == nil && len(noteLinks) > 0 {
		importEntity("note_links", func() (int, int, error) {
			return h.stores.Export.ImportNoteLinks(ctx, noteLinks)
		})
	}

	// 9. Memories
	var memories []models.Memory
	if err := ar.ReadEntityJSON("memories", &memories); err == nil && len(memories) > 0 {
		if remapUserID {
			for i := range memories {
				if memories[i].OwnerID == sourceUserID {
					memories[i].OwnerID = userID
				}
			}
		}
		importEntity("memories", func() (int, int, error) {
			return h.stores.Export.ImportMemories(ctx, memories)
		})
	}

	// 10. Project channels
	var projectChannels []models.ProjectChannel
	if err := ar.ReadEntityJSON("project_channels", &projectChannels); err == nil && len(projectChannels) > 0 {
		importEntity("project_channels", func() (int, int, error) {
			return h.stores.Export.ImportProjectChannels(ctx, projectChannels)
		})
	}

	// 10b. Project KBs
	var projectKBs []models.ProjectKB
	if err := ar.ReadEntityJSON("project_knowledge_bases", &projectKBs); err == nil && len(projectKBs) > 0 {
		importEntity("project_knowledge_bases", func() (int, int, error) {
			return h.stores.Export.ImportProjectKBs(ctx, projectKBs)
		})
	}

	// 10c. Project notes
	var projectNotes []models.ProjectNote
	if err := ar.ReadEntityJSON("project_notes", &projectNotes); err == nil && len(projectNotes) > 0 {
		importEntity("project_notes", func() (int, int, error) {
			return h.stores.Export.ImportProjectNotes(ctx, projectNotes)
		})
	}

	// 11. Workspace files
	var workspaceFiles []models.WorkspaceFile
	if err := ar.ReadEntityJSON("workspace_files", &workspaceFiles); err == nil && len(workspaceFiles) > 0 {
		importEntity("workspace_files", func() (int, int, error) {
			return h.stores.Export.ImportWorkspaceFiles(ctx, workspaceFiles)
		})
	}

	// 12. Files (metadata)
	var files []models.File
	if err := ar.ReadEntityJSON("files", &files); err == nil && len(files) > 0 {
		if remapUserID {
			for i := range files {
				if files[i].UserID == sourceUserID {
					files[i].UserID = userID
				}
			}
		}
		// Generate storage keys for imported files
		for i := range files {
			if files[i].StorageKey == "" && files[i].ChannelID != "" {
				files[i].StorageKey = fmt.Sprintf("files/%s/%s_%s", files[i].ChannelID, files[i].ID, files[i].Filename)
			}
		}
		importEntity("files", func() (int, int, error) {
			return h.stores.Export.ImportFiles(ctx, files)
		})
	}

	// 12b. File blobs from archive
	if h.objStore != nil {
		fileBlobs := ar.FileEntries()
		blobCount := 0
		for _, entry := range fileBlobs {
			if blobCount >= export.MaxExportFiles {
				errors = append(errors, "file blob limit reached, some files skipped")
				break
			}
			// Extract file ID from path: files/{fileID}/{filename}
			parts := strings.SplitN(strings.TrimPrefix(entry.Name, "files/"), "/", 2)
			if len(parts) != 2 {
				continue
			}
			fileID := parts[0]
			filename := parts[1]

			// Find matching file record for storage key
			storageKey := ""
			var contentType string
			for _, f := range files {
				if f.ID == fileID {
					storageKey = f.StorageKey
					contentType = f.ContentType
					break
				}
			}
			if storageKey == "" {
				storageKey = fmt.Sprintf("files/imported/%s_%s", fileID, filename)
			}
			if contentType == "" {
				contentType = "application/octet-stream"
			}

			rc, err := entry.Open()
			if err != nil {
				errors = append(errors, fmt.Sprintf("file %s: open error", filepath.Base(entry.Name)))
				continue
			}
			if err := h.objStore.Put(ctx, storageKey, rc, int64(entry.UncompressedSize64), contentType); err != nil {
				rc.Close()
				errors = append(errors, fmt.Sprintf("file %s: upload error", filepath.Base(entry.Name)))
				continue
			}
			rc.Close()
			blobCount++
		}
		if blobCount > 0 {
			imported["file_blobs"] = blobCount
		}
	}

	// 13. Settings/Prefs
	var userModelSettings []models.UserModelSetting
	if err := ar.ReadEntityJSON("user_settings", &userModelSettings); err == nil && len(userModelSettings) > 0 {
		if remapUserID {
			for i := range userModelSettings {
				if userModelSettings[i].UserID == sourceUserID {
					userModelSettings[i].UserID = userID
				}
			}
		}
		importEntity("user_settings", func() (int, int, error) {
			return h.stores.Export.ImportUserModelSettings(ctx, userModelSettings)
		})
	}

	var notifPrefs []models.NotificationPreference
	if err := ar.ReadEntityJSON("notification_preferences", &notifPrefs); err == nil && len(notifPrefs) > 0 {
		if remapUserID {
			for i := range notifPrefs {
				if notifPrefs[i].UserID == sourceUserID {
					notifPrefs[i].UserID = userID
				}
			}
		}
		importEntity("notification_preferences", func() (int, int, error) {
			return h.stores.Export.ImportNotifPrefs(ctx, notifPrefs)
		})
	}

	// 14. Persona groups
	var personaGroups []models.PersonaGroup
	if err := ar.ReadEntityJSON("persona_groups", &personaGroups); err == nil && len(personaGroups) > 0 {
		if remapUserID {
			for i := range personaGroups {
				if personaGroups[i].OwnerID == sourceUserID {
					personaGroups[i].OwnerID = userID
				}
			}
		}
		importEntity("persona_groups", func() (int, int, error) {
			return h.stores.Export.ImportPersonaGroups(ctx, personaGroups)
		})
	}

	var personaGroupMembers []models.PersonaGroupMember
	if err := ar.ReadEntityJSON("persona_group_members", &personaGroupMembers); err == nil && len(personaGroupMembers) > 0 {
		importEntity("persona_group_members", func() (int, int, error) {
			return h.stores.Export.ImportPersonaGroupMembers(ctx, personaGroupMembers)
		})
	}

	// ── Audit log ──
	h.stores.Audit.Log(ctx, &models.AuditEntry{
		ActorID:      &userID,
		Action:       "user.import",
		ResourceType: "user",
		ResourceID:   userID,
		Metadata:     models.JSONMap{"imported": imported, "skipped": skipped, "errors": errors},
	})

	c.JSON(http.StatusOK, gin.H{
		"imported": imported,
		"skipped":  skipped,
		"errors":   errors,
	})
}

// ImportTeam imports team data from a .switchboard archive.
// POST /api/v1/admin/teams/:id/import
func (h *DataImportHandler) ImportTeam(c *gin.Context) {
	ctx := c.Request.Context()
	userID := c.GetString("user_id")
	teamID := c.Param("id")

	// Verify team exists
	team, err := h.stores.Teams.GetByID(ctx, teamID)
	if err != nil || team == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "team not found"})
		return
	}

	// Receive upload
	file, header, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "missing file field"})
		return
	}
	defer file.Close()

	if header.Size > export.MaxExportArchiveSize {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("archive too large (max %d MB)", export.MaxExportArchiveSize/(1024*1024)),
		})
		return
	}

	tmp, err := os.CreateTemp("", "switchboard-team-import-*.zip")
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create temp file"})
		return
	}
	defer os.Remove(tmp.Name())
	defer tmp.Close()

	if _, err := io.Copy(tmp, file); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save upload"})
		return
	}
	tmp.Close()

	ar, err := export.OpenArchive(tmp.Name())
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid archive: " + err.Error()})
		return
	}
	defer ar.Close()

	manifest, err := ar.ReadManifest()
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid manifest: " + err.Error()})
		return
	}

	if manifest.FormatVersion > export.FormatVersion {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("unsupported format version %d", manifest.FormatVersion),
		})
		return
	}

	imported := make(map[string]int)
	skipped := make(map[string]int)
	var errors []string

	importEntity := func(name string, fn func() (int, int, error)) {
		imp, skip, err := fn()
		if err != nil {
			errors = append(errors, fmt.Sprintf("%s: %v", name, err))
			slog.Error("import: team entity error", "entity", name, "error", err)
		}
		if imp > 0 {
			imported[name] = imp
		}
		if skip > 0 {
			skipped[name] = skip
		}
	}

	// Remap team_id on entities to target team
	sourceTeamID := ""
	if manifest.Scope != nil {
		sourceTeamID = manifest.Scope.TeamID
	}
	remapTeam := sourceTeamID != "" && sourceTeamID != teamID

	// Channels (remap team_id)
	var channels []models.Channel
	if err := ar.ReadEntityJSON("channels", &channels); err == nil && len(channels) > 0 {
		if remapTeam {
			for i := range channels {
				if channels[i].TeamID != nil && *channels[i].TeamID == sourceTeamID {
					channels[i].TeamID = &teamID
				}
			}
		}
		importEntity("channels", func() (int, int, error) {
			return h.stores.Export.ImportChannels(ctx, channels)
		})
	}

	// Messages
	var messages []models.Message
	if err := ar.ReadEntityJSON("messages", &messages); err == nil && len(messages) > 0 {
		importEntity("messages", func() (int, int, error) {
			return h.stores.Export.ImportMessages(ctx, messages)
		})
	}

	// Projects (remap team_id)
	var projects []models.Project
	if err := ar.ReadEntityJSON("projects", &projects); err == nil && len(projects) > 0 {
		if remapTeam {
			for i := range projects {
				if projects[i].TeamID != nil && *projects[i].TeamID == sourceTeamID {
					projects[i].TeamID = &teamID
				}
			}
		}
		importEntity("projects", func() (int, int, error) {
			return h.stores.Export.ImportProjects(ctx, projects)
		})
	}

	// Persona groups (remap team_id)
	var personaGroups []models.PersonaGroup
	if err := ar.ReadEntityJSON("persona_groups", &personaGroups); err == nil && len(personaGroups) > 0 {
		if remapTeam {
			for i := range personaGroups {
				if personaGroups[i].TeamID != nil && *personaGroups[i].TeamID == sourceTeamID {
					personaGroups[i].TeamID = &teamID
				}
			}
		}
		importEntity("persona_groups", func() (int, int, error) {
			return h.stores.Export.ImportPersonaGroups(ctx, personaGroups)
		})
	}

	var personaGroupMembers []models.PersonaGroupMember
	if err := ar.ReadEntityJSON("persona_group_members", &personaGroupMembers); err == nil && len(personaGroupMembers) > 0 {
		importEntity("persona_group_members", func() (int, int, error) {
			return h.stores.Export.ImportPersonaGroupMembers(ctx, personaGroupMembers)
		})
	}

	// Audit log
	h.stores.Audit.Log(ctx, &models.AuditEntry{
		ActorID:      &userID,
		Action:       "team.import",
		ResourceType: "team",
		ResourceID:   teamID,
		Metadata:     models.JSONMap{"imported": imported, "skipped": skipped, "errors": errors},
	})

	c.JSON(http.StatusOK, gin.H{
		"imported": imported,
		"skipped":  skipped,
		"errors":   errors,
	})
}

// ImportChatGPT imports conversations from a ChatGPT export.
// POST /api/v1/import/chatgpt
func (h *DataImportHandler) ImportChatGPT(c *gin.Context) {
	ctx := c.Request.Context()
	userID := c.GetString("user_id")

	file, header, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "missing file field"})
		return
	}
	defer file.Close()

	if header.Size > export.MaxExportArchiveSize {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("file too large (max %d MB)", export.MaxExportArchiveSize/(1024*1024)),
		})
		return
	}

	// Detect if it's a zip or raw JSON
	var convs export.ChatGPTExport

	if strings.HasSuffix(header.Filename, ".zip") {
		// Save to temp and extract conversations.json
		tmp, err := os.CreateTemp("", "chatgpt-import-*.zip")
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create temp file"})
			return
		}
		defer os.Remove(tmp.Name())
		defer tmp.Close()

		if _, err := io.Copy(tmp, file); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save upload"})
			return
		}
		tmp.Close()

		ar, err := export.OpenArchive(tmp.Name())
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid zip archive"})
			return
		}
		defer ar.Close()

		// Try to find conversations.json in the zip
		if err := ar.ReadEntityJSON("conversations", &convs); err != nil || len(convs) == 0 {
			// Try searching all files for conversations.json
			for _, f := range ar.FileEntries() {
				if strings.HasSuffix(f.Name, "conversations.json") {
					rc, err := f.Open()
					if err != nil {
						continue
					}
					convs, err = export.ParseChatGPTExport(rc)
					rc.Close()
					if err == nil {
						break
					}
				}
			}
		}
	} else {
		// Raw JSON file
		convs, err = export.ParseChatGPTExport(file)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "failed to parse conversations.json: " + err.Error()})
			return
		}
	}

	if len(convs) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "no conversations found in upload"})
		return
	}

	// Convert to Switchboard models
	result := export.ConvertChatGPTExport(convs, userID)

	// Import channels and messages
	chImported, chSkipped, err := h.stores.Export.ImportChannels(ctx, result.Channels)
	if err != nil {
		slog.Error("chatgpt import: channels", "error", err)
	}

	msgImported, msgSkipped, err := h.stores.Export.ImportMessages(ctx, result.Messages)
	if err != nil {
		slog.Error("chatgpt import: messages", "error", err)
	}

	imported := map[string]int{
		"channels": chImported,
		"messages": msgImported,
	}
	skipped := map[string]int{
		"channels": chSkipped,
		"messages": msgSkipped,
	}

	h.stores.Audit.Log(ctx, &models.AuditEntry{
		ActorID:      &userID,
		Action:       "user.import_chatgpt",
		ResourceType: "user",
		ResourceID:   userID,
		Metadata: models.JSONMap{
			"conversations": len(convs),
			"imported":      imported,
			"skipped":       skipped,
		},
	})

	c.JSON(http.StatusOK, gin.H{
		"conversations": len(convs),
		"imported":      imported,
		"skipped":       skipped,
	})
}
