package kb

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/store/content"
	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/gorm"
)

type UADashboardStatus struct {
	State                    string
	SourceID                 string
	SourceCommitSHA          string
	DashboardSourceCommitSHA string
	DashboardCommitSHA       string
	UpdateAvailable          bool
	PublishedAt              *time.Time
	CanImport                bool
	AcceptedFormats          []string
	Limits                   UADashboardImportLimits
	Manifest                 *UADashboardManifest
}

type UADashboardImport struct {
	ArchivePath string
	Filename    string
}

type UADashboardFile struct {
	Content []byte
	ETag    string
}

// UADashboardSourcePreview identifies the source-code read plane for a
// Dashboard publication. It is resolved only when a caller explicitly opens
// source preview; it is intentionally not part of Dashboard status/first paint.
type UADashboardSourcePreview struct {
	RepoOwner       string
	RepoName        string
	SourceCommitSHA string
}

func (s *KBService) AuthorizeUADashboardImport(ctx context.Context, workspaceID, subject, kbID, sourceID string) (UADashboardImportLimits, error) {
	_, err := s.uaDashboardSnapshot(ctx, workspaceID, subject, kbID, sourceID, true)
	return s.uaDashboardLimits, err
}

type uaDashboardSnapshot struct {
	base                storekb.KnowledgeBase
	source              storekb.Source
	revision            storekb.SourceRevision
	dashboardRevision   storekb.SourceRevision
	repository          repos.ResourceRepository
	dashboardRepository UADashboardRepository
	role                string
	native              bool
}

func (s *KBService) GetUADashboardStatus(ctx context.Context, workspaceID, subject, kbID, sourceID string) (UADashboardStatus, error) {
	snapshot, err := s.uaDashboardSnapshot(ctx, workspaceID, subject, kbID, sourceID, false)
	if err != nil {
		return UADashboardStatus{}, err
	}
	canImport := false
	if roleCanEdit(snapshot.role) {
		canImport = s.canAdministerWorkspaceForRead(ctx, workspaceID, subject)
	}
	return s.uaDashboardStatus(snapshot.dashboardRevision, canImport)
}

func (s *KBService) GetUADashboardSourcePreview(ctx context.Context, workspaceID, subject, kbID, sourceID string) (UADashboardSourcePreview, error) {
	snapshot, err := s.uaDashboardSnapshot(ctx, workspaceID, subject, kbID, sourceID, false)
	if err != nil {
		return UADashboardSourcePreview{}, err
	}
	if snapshot.native {
		return UADashboardSourcePreview{}, codedConflict("dashboard-source-preview-unavailable", "Dashboard source preview is unavailable after native activation", nil)
	}
	for _, role := range []string{repos.RepoRolePrimary, repos.RepoRoleMirror} {
		repo, repoErr := s.repositoryStore.Get(ctx, workspaceID, repos.ResourceTypeKBSource, sourceID, role)
		if repoErr == nil {
			return UADashboardSourcePreview{RepoOwner: repo.RepoOwner, RepoName: repo.RepoName, SourceCommitSHA: snapshot.revision.SourceCommitSHA}, nil
		}
		if !errors.Is(repoErr, gorm.ErrRecordNotFound) {
			return UADashboardSourcePreview{}, repoErr
		}
	}
	return UADashboardSourcePreview{}, codedNotFound("dashboard-source-preview-unavailable", "Dashboard source preview is unavailable")
}

func (s *KBService) ImportUADashboard(ctx context.Context, workspaceID, subject, kbID, sourceID string, input UADashboardImport) (UADashboardStatus, error) {
	snapshot, err := s.uaDashboardSnapshot(ctx, workspaceID, subject, kbID, sourceID, true)
	if err != nil {
		return UADashboardStatus{}, err
	}
	staged, err := StageUADashboardArchive(ctx, input.ArchivePath, input.Filename, s.uaDashboardLimits)
	if err != nil {
		return UADashboardStatus{}, mapUADashboardImportError(err)
	}
	if snapshot.dashboardRepository == nil {
		return UADashboardStatus{}, codedConflict("dashboard-insights-unavailable", "Dashboard publication store is unavailable", nil)
	}
	published, err := snapshot.dashboardRepository.PublishUADashboard(ctx, UADashboardRepositoryPublishRequest{
		Repository: snapshot.repository, WorkspaceID: snapshot.base.WorkspaceID, SourceID: snapshot.source.ID,
		SourceRevisionID: snapshot.revision.ID, PreviousPublicationID: snapshot.dashboardRevision.UADashboardCommitSHA,
		Actor: subject, Files: staged.Files, Limits: s.uaDashboardLimits,
	})
	if err != nil {
		return UADashboardStatus{}, fmt.Errorf("publish dashboard content: %w", err)
	}
	manifest := staged.Manifest
	manifest.SourceCommitSHA = snapshot.revision.SourceCommitSHA
	manifest.DashboardCommitSHA = published.CommitSHA
	for index := range manifest.Files {
		manifest.Files[index].BlobSHA = strings.TrimSpace(published.FileDigests[manifest.Files[index].Path])
		if manifest.Files[index].BlobSHA == "" {
			return UADashboardStatus{}, fmt.Errorf("dashboard publication digest is incomplete")
		}
	}
	manifestJSON, err := json.Marshal(manifest)
	if err != nil || len(manifestJSON) > storekb.UADashboardManifestMaxBytes {
		return UADashboardStatus{}, fmt.Errorf("serialize dashboard manifest")
	}
	publishedAt := s.now()
	auditDetails, err := json.Marshal(map[string]any{
		"archiveFormat": staged.Format, "sourceCommitSha": snapshot.revision.SourceCommitSHA,
		"dashboardCommitSha": published.CommitSHA, "files": len(manifest.Files), "bytes": manifestByteCount(manifest),
	})
	if err != nil || len(auditDetails) > storekb.UADashboardManifestMaxBytes {
		return UADashboardStatus{}, fmt.Errorf("serialize dashboard audit details")
	}
	revision, swapped, err := s.store.PublishUADashboard(ctx, storekb.PublishUADashboardParams{
		WorkspaceID: snapshot.base.WorkspaceID, KnowledgeBaseID: snapshot.base.ID, SourceID: snapshot.source.ID,
		RevisionID: snapshot.revision.ID, SourceCommitSHA: snapshot.revision.SourceCommitSHA,
		PreviousCommitSHA: snapshot.revision.UADashboardCommitSHA, DashboardCommitSHA: published.CommitSHA,
		ManifestJSON: string(manifestJSON), PublishedAt: publishedAt,
		AuditEvent: s.auditEvent(snapshot.base.WorkspaceID, snapshot.base.ID, subject, "kb.source.dashboard.import", "source", snapshot.source.ID, "Imported Understand Anything dashboard", string(auditDetails)),
	})
	if err != nil {
		return UADashboardStatus{}, fmt.Errorf("commit dashboard publication: %w", err)
	}
	if !swapped {
		return UADashboardStatus{}, codedConflict("dashboard-import-conflict", "Dashboard changed during import", nil)
	}
	return s.uaDashboardStatus(revision, true)
}

func (s *KBService) ReadUADashboardFile(ctx context.Context, workspaceID, subject, kbID, sourceID, path string) (UADashboardFile, error) {
	snapshot, err := s.uaDashboardSnapshot(ctx, workspaceID, subject, kbID, sourceID, false)
	if err != nil {
		return UADashboardFile{}, err
	}
	if snapshot.dashboardRepository == nil {
		return UADashboardFile{}, codedConflict("dashboard-insights-unavailable", "Dashboard publication store is unavailable", nil)
	}
	manifest, state, err := classifyUADashboardManifest(snapshot.dashboardRevision)
	if err != nil {
		return UADashboardFile{}, codedConflict("dashboard-publication-invalid", "Dashboard publication metadata is invalid", nil)
	}
	if state == "reimport_required" {
		return UADashboardFile{}, codedConflict("dashboard-reimport-required", "Dashboard must be re-imported before files can be read", nil)
	}
	var entry *UADashboardManifestFile
	for index := range manifest.Files {
		if manifest.Files[index].Path == path {
			entry = &manifest.Files[index]
			break
		}
	}
	if entry == nil {
		return UADashboardFile{}, codedNotFound("dashboard-file-not-found", "Dashboard file is not published")
	}
	file, err := snapshot.dashboardRepository.ReadUADashboardFile(ctx, UADashboardRepositoryFileRequest{
		Repository: snapshot.repository, DashboardCommitSHA: snapshot.dashboardRevision.UADashboardCommitSHA,
		WorkspaceID: snapshot.base.WorkspaceID, SourceID: snapshot.source.ID, Subject: subject,
		Path: path, ExpectedBlobSHA: entry.BlobSHA, MaxBytes: s.uaDashboardLimits.FileBytes,
	})
	if err != nil {
		return UADashboardFile{}, fmt.Errorf("read dashboard file: %w", err)
	}
	return UADashboardFile{Content: file.Content, ETag: `"` + snapshot.dashboardRevision.UADashboardCommitSHA + ":" + file.BlobSHA + `"`}, nil
}

func (s *KBService) uaDashboardSnapshot(ctx context.Context, workspaceID, subject, kbID, sourceID string, write bool) (uaDashboardSnapshot, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return uaDashboardSnapshot{}, err
	}
	base, err := s.store.GetKnowledgeBase(ctx, workspaceID, kbID)
	if err != nil {
		return uaDashboardSnapshot{}, mapStoreNotFound(err)
	}
	role := s.knowledgeBaseRole(ctx, base, subject)
	if role == "" {
		return uaDashboardSnapshot{}, forbidden("knowledge base is not accessible")
	}
	if write && !roleCanEdit(role) {
		return uaDashboardSnapshot{}, codedForbidden("dashboard-import-forbidden", "Dashboard import requires KB edit access and workspace administrator authority")
	}
	if write {
		allowed, governanceErr := s.canAdministerWorkspace(ctx, workspaceID, subject)
		if governanceErr != nil {
			return uaDashboardSnapshot{}, governanceErr
		}
		if !allowed {
			return uaDashboardSnapshot{}, codedForbidden("dashboard-import-forbidden", "Dashboard import requires KB edit access and workspace administrator authority")
		}
	}
	source, err := s.store.GetSource(ctx, workspaceID, kbID, sourceID)
	if err != nil {
		return uaDashboardSnapshot{}, mapStoreNotFound(err)
	}
	revision, err := s.store.GetCurrentSourceRevision(ctx, workspaceID, kbID, sourceID)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return uaDashboardSnapshot{}, codedConflict("dashboard-source-revision-unavailable", "Source has no current revision", nil)
	}
	if err != nil {
		return uaDashboardSnapshot{}, err
	}
	if strings.TrimSpace(revision.SourceCommitSHA) == "" {
		return uaDashboardSnapshot{}, codedConflict("dashboard-source-revision-unavailable", "Source revision is not immutable", nil)
	}
	state := content.AuthorityStateLegacyActive
	authorityCheckpoint := ""
	if s.contentAuthority != nil {
		authorities, authorityErr := s.contentAuthority.ListAuthoritiesForBindings(ctx, workspaceID, kbSourceContentDomain, []string{sourceID})
		if authorityErr != nil {
			return uaDashboardSnapshot{}, fmt.Errorf("resolve Dashboard source authority: %w", authorityErr)
		}
		if authority, ok := authorities[sourceID]; ok {
			state = strings.TrimSpace(authority.State)
			authorityCheckpoint = authority.Checkpoint
		}
	}
	if state == content.AuthorityStateNativeActive {
		dashboardRevision, routeErr := nativeUADashboardRevision(revision, authorityCheckpoint)
		if routeErr != nil {
			return uaDashboardSnapshot{}, codedConflict("dashboard-authority-invalid", "Dashboard source authority is invalid", routeErr)
		}
		return uaDashboardSnapshot{
			base: base, source: source, revision: revision, dashboardRevision: dashboardRevision,
			dashboardRepository: s.nativeUADashboardRepository, role: role, native: true,
		}, nil
	}
	if state != content.AuthorityStateLegacyActive {
		return uaDashboardSnapshot{}, codedConflict("dashboard-authority-invalid", "Dashboard source authority is invalid", nil)
	}
	if s.repositoryStore == nil || (write && s.uaDashboardRepository == nil) {
		return uaDashboardSnapshot{}, codedConflict("dashboard-insights-unavailable", "Dashboard insights repository is unavailable", nil)
	}
	repo, err := s.repositoryStore.Get(ctx, workspaceID, repos.ResourceTypeKBSource, sourceID, repos.RepoRoleInsights)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return uaDashboardSnapshot{}, codedConflict("dashboard-insights-unavailable", "Dashboard insights repository is unavailable", nil)
	}
	if err != nil {
		return uaDashboardSnapshot{}, err
	}
	return uaDashboardSnapshot{
		base: base, source: source, revision: revision, dashboardRevision: revision, repository: repo,
		dashboardRepository: s.uaDashboardRepository, role: role,
	}, nil
}

func nativeUADashboardRevision(revision storekb.SourceRevision, authorityCheckpoint string) (storekb.SourceRevision, error) {
	publicationID := strings.TrimSpace(revision.UADashboardCommitSHA)
	if publicationID == "" || validKBSourceCutoverVersionID(publicationID) {
		return revision, nil
	}
	checkpoint, err := decodeKBSourceCutoverCheckpoint(authorityCheckpoint)
	if err != nil || checkpoint.UADashboardVersionID == "" {
		return storekb.SourceRevision{}, fmt.Errorf("native Dashboard publication is missing from the cutover checkpoint")
	}
	manifest, state, err := classifyUADashboardManifest(revision)
	if err != nil || state != "ready" {
		return storekb.SourceRevision{}, fmt.Errorf("legacy Dashboard publication is not migratable")
	}
	manifest.DashboardCommitSHA = checkpoint.UADashboardVersionID
	encoded, err := json.Marshal(manifest)
	if err != nil || len(encoded) > storekb.UADashboardManifestMaxBytes {
		return storekb.SourceRevision{}, fmt.Errorf("native Dashboard manifest is invalid")
	}
	revision.UADashboardCommitSHA = checkpoint.UADashboardVersionID
	revision.UADashboardManifestJSON = string(encoded)
	return revision, nil
}

func (s *KBService) uaDashboardStatus(revision storekb.SourceRevision, canImport bool) (UADashboardStatus, error) {
	status := UADashboardStatus{State: "empty", SourceID: revision.SourceID, SourceCommitSHA: revision.SourceCommitSHA, CanImport: canImport, AcceptedFormats: []string{"zip", "tar.gz", "tgz"}, Limits: s.uaDashboardLimits}
	if strings.TrimSpace(revision.UADashboardCommitSHA) == "" {
		return status, nil
	}
	manifest, state, err := classifyUADashboardManifest(revision)
	if err != nil {
		return UADashboardStatus{}, codedConflict("dashboard-publication-invalid", "Dashboard publication metadata is invalid", nil)
	}
	status.State = state
	status.SourceCommitSHA = revision.SourceCommitSHA
	status.DashboardSourceCommitSHA = manifest.SourceCommitSHA
	status.DashboardCommitSHA = revision.UADashboardCommitSHA
	status.UpdateAvailable = manifest.SourceCommitSHA != revision.SourceCommitSHA
	status.PublishedAt = revision.UADashboardPublishedAt
	if state == "ready" {
		status.Manifest = &manifest
	}
	return status, nil
}

func classifyUADashboardManifest(revision storekb.SourceRevision) (UADashboardManifest, string, error) {
	if strings.TrimSpace(revision.UADashboardCommitSHA) == "" || len(revision.UADashboardManifestJSON) == 0 || len(revision.UADashboardManifestJSON) > storekb.UADashboardManifestMaxBytes || revision.UADashboardPublishedAt == nil {
		return UADashboardManifest{}, "", fmt.Errorf("stored dashboard manifest is invalid")
	}
	var manifest UADashboardManifest
	if err := json.Unmarshal([]byte(revision.UADashboardManifestJSON), &manifest); err != nil || manifest.SchemaVersion != 1 || strings.TrimSpace(manifest.SourceCommitSHA) == "" || manifest.DashboardCommitSHA != revision.UADashboardCommitSHA {
		return UADashboardManifest{}, "", fmt.Errorf("stored dashboard manifest is invalid")
	}
	paths := make([]string, len(manifest.Files))
	for index, file := range manifest.Files {
		paths[index] = file.Path
		if file.Bytes < 0 || strings.TrimSpace(file.BlobSHA) == "" {
			return UADashboardManifest{}, "", fmt.Errorf("stored dashboard manifest is invalid")
		}
	}
	if hasExactUADashboardPaths(paths) {
		return manifest, "ready", nil
	}
	if hasKnownHistoricalUADashboardPaths(paths) {
		return manifest, "reimport_required", nil
	}
	return UADashboardManifest{}, "", fmt.Errorf("stored dashboard manifest is invalid")
}

func manifestByteCount(manifest UADashboardManifest) int64 {
	var total int64
	for _, file := range manifest.Files {
		total += file.Bytes
	}
	return total
}

func mapUADashboardImportError(err error) error {
	switch {
	case errors.Is(err, ErrUADashboardArchiveUnsupported), errors.Is(err, ErrUADashboardArchiveUnsafe), errors.Is(err, ErrUADashboardArchiveLayout):
		return codedInvalidArgument(firstUADashboardErrorCode(err), "Dashboard archive is invalid")
	case errors.Is(err, ErrUADashboardArchiveLimit):
		return domainError{kind: ErrInvalidArgument, msg: "Dashboard archive exceeds limits", code: "dashboard-archive-limit-exceeded", details: map[string]any{"httpStatus": 413}}
	case errors.Is(err, ErrUADashboardDataInvalid):
		return domainError{kind: ErrInvalidArgument, msg: "Dashboard data is invalid", code: "dashboard-data-invalid", details: map[string]any{"httpStatus": 422}}
	default:
		return err
	}
}

func firstUADashboardErrorCode(err error) string {
	for _, candidate := range []error{ErrUADashboardArchiveUnsupported, ErrUADashboardArchiveUnsafe, ErrUADashboardArchiveLayout} {
		if errors.Is(err, candidate) {
			return candidate.Error()
		}
	}
	return "dashboard-archive-invalid"
}
