package story_service

import (
	"context"
	"evening_detective/internal/modules/cleaner"
	"evening_detective/internal/modules/formatter"
	"evening_detective/internal/services/link"
	"evening_detective/internal/services/story_service/models"
	"regexp"
	"strings"
)

type StoryService struct {
	cleaner      cleaner.ICleaner
	formatter    formatter.IFormatter
	story        *models.Story
	storyStorage IStoryStorage
	linkService  link.ILinkService
}

func NewStoryService(
	cleaner cleaner.ICleaner,
	formatter formatter.IFormatter,
	storyStorage IStoryStorage,
	linkService link.ILinkService,
) (*StoryService, error) {
	s := &StoryService{
		cleaner:      cleaner,
		formatter:    formatter,
		storyStorage: storyStorage,
		linkService:  linkService,
	}
	story, err := s.storyStorage.Load(context.Background())
	if err != nil {
		return nil, err
	}
	s.story = story
	return s, nil
}

func (s *StoryService) Update(ctx context.Context) error {
	if err := s.storyStorage.Save(ctx, s.story); err != nil {
		return err
	}
	story, err := s.storyStorage.Load(ctx)
	if err != nil {
		return err
	}
	s.story = story
	return nil
}

func (s *StoryService) GetPlace(code string) *models.Place {
	if strings.HasPrefix(code, "[") || strings.HasSuffix(code, "]") {
		return models.NewClientErrorPlace(code)
	}
	clearCode := s.cleaner.ClearCode(code)
	for _, place := range s.story.Places {
		if s.cleaner.ClearCode(place.Code) == clearCode {
			applications := make([]*models.Application, 0, len(place.Applications))
			for _, application := range place.Applications {
				applications = append(
					applications,
					models.NewApplication(
						s.cleaner.ClearText(application.Name),
					),
				)
			}
			doors := make([]*models.Door, 0, len(place.Doors))
			for _, door := range place.Doors {
				doors = append(
					doors,
					models.NewDoor(
						door.Code,
						door.Name,
						models.WithDoorShow(door.Show),
					),
				)
			}
			return models.NewPlace(
				place.Code,
				place.Name,
				s.cleaner.ClearText(place.Text),
				models.WithPlaceImage(s.linkService.GetImageLink(place.Image)),
				models.WithPlaceApplication(applications...),
				models.WithPlaceHidden(place.Hidden),
				models.WithPlaceDoors(doors...),
			)
		}
	}
	return models.NewNotFoundPlace(code)
}

func (s *StoryService) GetPlaces(codes []string) []*models.Place {
	places := make([]*models.Place, 0, 100)
	mOpen := map[string]any{}
	mDeleted := map[string]any{}
	applicationNumber := 1
	applicationsMap := make(map[string]interface{}, 10)
	for i, code := range codes {
		place := s.GetPlace(code)
		for i, application := range place.Applications {
			if _, ok := applicationsMap[application.Name]; ok {
				place.Applications = append(place.Applications[:i], place.Applications[i+1:]...)
				if len(place.Applications) == 0 {
					place.Applications = nil
				}
			}
			applicationsMap[application.Name] = struct{}{}
			application.SetNumber(applicationNumber)
			applicationNumber++
		}
		if _, ok := mOpen[place.Code]; place.Hidden && !ok {
			place = models.NewNotFoundPlace(place.Code)
		}
		places = append(places, place)
		for j, door := range places[i].Doors {
			if _, ok := mDeleted[door.Code]; ok {
				places[i].Doors[j].Show = false
				continue
			}
			mOpen[door.Code] = struct{}{}
		}
		if i > 0 {
			for j, door := range places[i-1].Doors {
				if door.Code != place.Code && door.Show {
					places[i-1].Doors[j].Show = false
					delete(mOpen, door.Code)
					mDeleted[door.Code] = struct{}{}
				}
			}
		}
	}
	return places
}

func (s *StoryService) UpdatePlace(ctx context.Context, code string, node *GraphNode) error {
	if code != "" && node.Code == "" {
		return s.deletePlace(ctx, code)
	}
	if code == "" && node.Code != "" {
		return s.addPlace(ctx, node)
	}
	if code == "" || node.Code == "" {
		return nil
	}
	return s.updatePlace(ctx, code, node)
}

func (s *StoryService) deletePlace(ctx context.Context, code string) error {
	for i := range s.story.Places {
		if s.story.Places[i].Code == code {
			s.story.Places = append(s.story.Places[:i], s.story.Places[i+1:]...)
			break
		}
	}
	return s.Update(ctx)
}

func (s *StoryService) addPlace(ctx context.Context, node *GraphNode) error {
	s.story.Places = append(
		s.story.Places,
		models.NewPlace(
			node.Code,
			node.Name,
			s.formatter.FormatText(node.Text),
			models.WithPlaceApplication(s.getApplications(node)...),
		),
	)
	return s.Update(ctx)
}

func (s *StoryService) updatePlace(ctx context.Context, code string, node *GraphNode) error {
	for i := range s.story.Places {
		if s.story.Places[i].Code == code {
			s.story.Places[i] = models.NewPlace(
				node.Code,
				node.Name,
				s.formatter.FormatText(node.Text),
				models.WithPlaceApplication(s.getApplications(node)...),
			)
			return s.Update(ctx)
		}
	}
	for i := range s.story.Places {
		if s.story.Places[i].Code == node.Code {
			s.story.Places[i] = models.NewPlace(
				code,
				node.Name,
				s.formatter.FormatText(node.Text),
				models.WithPlaceApplication(s.getApplications(node)...),
			)
			break
		}
	}
	return s.Update(ctx)
}

func (s *StoryService) getApplications(node *GraphNode) []*models.Application {
	nodeApplications := make([]*models.Application, 0, len(node.Applications))
	for _, application := range node.Applications {
		nodeApplications = append(
			nodeApplications,
			&models.Application{
				Name: application.Name,
			},
		)
	}
	return nodeApplications
}

func (s *StoryService) GetGraph(ctx context.Context) *Graph {
	m := make(map[string]string, len(s.story.Places))
	nodes := make([]*GraphNode, 0, len(s.story.Places))
	for _, place := range s.story.Places {
		m[s.cleaner.ClearCode(place.Code)] = place.Code
		applications := make([]*GraphApplication, 0, len(place.Applications))
		for _, application := range place.Applications {
			applications = append(
				applications,
				&GraphApplication{
					Name: application.Name,
				},
			)
		}
		nodes = append(
			nodes, &GraphNode{
				Code:         place.Code,
				Name:         place.Name,
				Text:         place.Text,
				Applications: applications,
			},
		)
	}

	edges := make([]*GraphEdge, 0, len(s.story.Places)*3)
	for _, place := range s.story.Places {
		placeLinks := s.findPlaceLinksInText(place.Text)
		for _, placeLink := range placeLinks {
			edges = append(
				edges,
				&GraphEdge{
					From: m[s.cleaner.ClearCode(place.Code)],
					To:   m[s.cleaner.ClearCode(placeLink)],
					Type: "node",
				},
			)
		}
		for _, application := range place.Applications {
			placeLinks := s.findPlaceLinksInText(application.Name)
			for _, placeLink := range placeLinks {
				edges = append(
					edges,
					&GraphEdge{
						From: m[s.cleaner.ClearCode(place.Code)],
						To:   m[s.cleaner.ClearCode(placeLink)],
						Type: "application",
					},
				)
			}
		}
	}

	return &Graph{
		Nodes: nodes,
		Edges: edges,
	}
}

func (s *StoryService) findPlaceLinksInText(text string) []string {
	re := regexp.MustCompile(`\(\[[a-zA-Zа-яА-Я\d-]+\]\)`)
	return re.FindAllString(text, -1)
}
