package service

import (
	"context"
	"net/http"
	"strings"

	"cmr-backend/internal/apperr"
	"cmr-backend/internal/store/postgres"
)

type AdminPipelineService struct {
	store         *postgres.Store
	configService *ConfigService
}

type AdminReleaseView struct {
	ID                     string                    `json:"id"`
	ReleaseNo              int                       `json:"releaseNo"`
	ConfigLabel            string                    `json:"configLabel"`
	ManifestURL            string                    `json:"manifestUrl"`
	ManifestChecksumSha256 *string                   `json:"manifestChecksumSha256,omitempty"`
	RouteCode              *string                   `json:"routeCode,omitempty"`
	BuildID                *string                   `json:"buildId,omitempty"`
	Status                 string                    `json:"status"`
	PublishedAt            string                    `json:"publishedAt"`
	Runtime                *RuntimeSummaryView       `json:"runtime,omitempty"`
	Presentation           *PresentationSummaryView  `json:"presentation,omitempty"`
	ContentBundle          *ContentBundleSummaryView `json:"contentBundle,omitempty"`
}

type AdminEventPipelineView struct {
	EventID        string                  `json:"eventId"`
	CurrentRelease *AdminReleaseView       `json:"currentRelease,omitempty"`
	Sources        []EventConfigSourceView `json:"sources"`
	Builds         []EventConfigBuildView  `json:"builds"`
	Releases       []AdminReleaseView      `json:"releases"`
}

type AdminRollbackReleaseInput struct {
	ReleaseID string `json:"releaseId"`
}

type AdminBindReleaseRuntimeInput struct {
	RuntimeBindingID string `json:"runtimeBindingId"`
}

type AdminPublishBuildInput struct {
	RuntimeBindingID string `json:"runtimeBindingId,omitempty"`
	PresentationID   string `json:"presentationId,omitempty"`
	ContentBundleID  string `json:"contentBundleId,omitempty"`
}

func NewAdminPipelineService(store *postgres.Store, configService *ConfigService) *AdminPipelineService {
	return &AdminPipelineService{
		store:         store,
		configService: configService,
	}
}

func (s *AdminPipelineService) GetEventPipeline(ctx context.Context, eventPublicID string, limit int) (*AdminEventPipelineView, error) {
	event, err := s.store.GetEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if event == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}

	sources, err := s.configService.ListEventConfigSources(ctx, event.PublicID, limit)
	if err != nil {
		return nil, err
	}
	buildRecords, err := s.store.ListEventConfigBuildsByEventID(ctx, event.ID, limit)
	if err != nil {
		return nil, err
	}
	releaseRecords, err := s.store.ListEventReleasesByEventID(ctx, event.ID, limit)
	if err != nil {
		return nil, err
	}

	builds := make([]EventConfigBuildView, 0, len(buildRecords))
	for i := range buildRecords {
		item, err := buildEventConfigBuildView(&buildRecords[i])
		if err != nil {
			return nil, err
		}
		builds = append(builds, *item)
	}
	releases := make([]AdminReleaseView, 0, len(releaseRecords))
	for _, item := range releaseRecords {
		view := buildAdminReleaseView(item)
		if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, item.PresentationID); err != nil {
			return nil, err
		} else if enrichedPresentation != nil {
			view.Presentation = enrichedPresentation
		}
		if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, item.ContentBundleID); err != nil {
			return nil, err
		} else if enrichedBundle != nil {
			view.ContentBundle = enrichedBundle
		}
		releases = append(releases, view)
	}

	result := &AdminEventPipelineView{
		EventID:  event.PublicID,
		Sources:  sources,
		Builds:   builds,
		Releases: releases,
	}
	if event.CurrentReleasePubID != nil {
		result.CurrentRelease = &AdminReleaseView{
			ID:                     *event.CurrentReleasePubID,
			ConfigLabel:            derefStringOrEmpty(event.ConfigLabel),
			ManifestURL:            derefStringOrEmpty(event.ManifestURL),
			ManifestChecksumSha256: event.ManifestChecksum,
			RouteCode:              event.RouteCode,
			Status:                 "published",
			Runtime:                buildRuntimeSummaryFromEvent(event),
			Presentation:           buildPresentationSummaryFromEvent(event),
			ContentBundle:          buildContentBundleSummaryFromEvent(event),
		}
		if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
			return nil, err
		} else if enrichedPresentation != nil {
			result.CurrentRelease.Presentation = enrichedPresentation
		}
		if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
			return nil, err
		} else if enrichedBundle != nil {
			result.CurrentRelease.ContentBundle = enrichedBundle
		}
	}
	return result, nil
}

func (s *AdminPipelineService) BuildSource(ctx context.Context, sourceID string) (*EventConfigBuildView, error) {
	return s.configService.BuildPreview(ctx, BuildPreviewInput{SourceID: sourceID})
}

func (s *AdminPipelineService) GetBuild(ctx context.Context, buildID string) (*EventConfigBuildView, error) {
	return s.configService.GetEventConfigBuild(ctx, buildID)
}

func (s *AdminPipelineService) PublishBuild(ctx context.Context, buildID string, input AdminPublishBuildInput) (*PublishedReleaseView, error) {
	return s.configService.PublishBuild(ctx, PublishBuildInput{
		BuildID:          buildID,
		RuntimeBindingID: input.RuntimeBindingID,
		PresentationID:   input.PresentationID,
		ContentBundleID:  input.ContentBundleID,
	})
}

func (s *AdminPipelineService) GetRelease(ctx context.Context, releasePublicID string) (*AdminReleaseView, error) {
	release, err := s.store.GetEventReleaseByPublicID(ctx, strings.TrimSpace(releasePublicID))
	if err != nil {
		return nil, err
	}
	if release == nil {
		return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found")
	}
	view := buildAdminReleaseView(*release)
	if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, release.PresentationID); err != nil {
		return nil, err
	} else if enrichedPresentation != nil {
		view.Presentation = enrichedPresentation
	}
	if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, release.ContentBundleID); err != nil {
		return nil, err
	} else if enrichedBundle != nil {
		view.ContentBundle = enrichedBundle
	}
	return &view, nil
}

func (s *AdminPipelineService) BindReleaseRuntime(ctx context.Context, releasePublicID string, input AdminBindReleaseRuntimeInput) (*AdminReleaseView, error) {
	release, err := s.store.GetEventReleaseByPublicID(ctx, strings.TrimSpace(releasePublicID))
	if err != nil {
		return nil, err
	}
	if release == nil {
		return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found")
	}

	input.RuntimeBindingID = strings.TrimSpace(input.RuntimeBindingID)
	if input.RuntimeBindingID == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "runtimeBindingId is required")
	}

	runtimeBinding, err := s.store.GetMapRuntimeBindingByPublicID(ctx, input.RuntimeBindingID)
	if err != nil {
		return nil, err
	}
	if runtimeBinding == nil {
		return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
	}
	if runtimeBinding.EventID != release.EventID {
		return nil, apperr.New(http.StatusConflict, "runtime_binding_not_belong_to_event", "runtime binding does not belong to release event")
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	if err := s.store.SetEventReleaseRuntimeBinding(ctx, tx, release.ID, &runtimeBinding.ID); err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	updated, err := s.store.GetEventReleaseByPublicID(ctx, release.PublicID)
	if err != nil {
		return nil, err
	}
	if updated == nil {
		return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found")
	}
	view := buildAdminReleaseView(*updated)
	return &view, nil
}

func (s *AdminPipelineService) RollbackRelease(ctx context.Context, eventPublicID string, input AdminRollbackReleaseInput) (*AdminReleaseView, error) {
	event, err := s.store.GetEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if event == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}

	input.ReleaseID = strings.TrimSpace(input.ReleaseID)
	if input.ReleaseID == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "releaseId is required")
	}

	release, err := s.store.GetEventReleaseByPublicID(ctx, input.ReleaseID)
	if err != nil {
		return nil, err
	}
	if release == nil {
		return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found")
	}
	if release.EventID != event.ID {
		return nil, apperr.New(http.StatusConflict, "release_not_belong_to_event", "release does not belong to event")
	}
	if release.Status != "published" {
		return nil, apperr.New(http.StatusConflict, "release_not_publishable", "release is not published")
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	if err := s.store.SetCurrentEventRelease(ctx, tx, event.ID, release.ID); err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	view := buildAdminReleaseView(*release)
	return &view, nil
}

func buildAdminReleaseView(item postgres.EventRelease) AdminReleaseView {
	return AdminReleaseView{
		ID:                     item.PublicID,
		ReleaseNo:              item.ReleaseNo,
		ConfigLabel:            item.ConfigLabel,
		ManifestURL:            item.ManifestURL,
		ManifestChecksumSha256: item.ManifestChecksum,
		RouteCode:              item.RouteCode,
		BuildID:                item.BuildID,
		Status:                 item.Status,
		PublishedAt:            item.PublishedAt.Format(timeRFC3339),
		Runtime:                buildRuntimeSummaryFromRelease(&item),
		Presentation:           buildPresentationSummaryFromRelease(&item),
		ContentBundle:          buildContentBundleSummaryFromRelease(&item),
	}
}

func derefStringOrEmpty(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}
