package app

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"

	"qctextbuilder/internal/buildsvc"
	"qctextbuilder/internal/config"
	"qctextbuilder/internal/domain"
	"qctextbuilder/internal/draftsvc"
	"qctextbuilder/internal/httpserver"
	"qctextbuilder/internal/httpserver/handlers"
	"qctextbuilder/internal/httpserver/views"
	"qctextbuilder/internal/llmruntime"
	"qctextbuilder/internal/logging"
	"qctextbuilder/internal/mapping"
	"qctextbuilder/internal/onboarding"
	"qctextbuilder/internal/polling"
	"qctextbuilder/internal/qcclient"
	"qctextbuilder/internal/store"
	"qctextbuilder/internal/store/memory"
	"qctextbuilder/internal/store/sqlite"
	"qctextbuilder/internal/templatesvc"
)

type App struct {
	server     *httpserver.Server
	pollingSvc *polling.Service
}

func New(cfg config.Config) (*App, error) {
	logSetup := logging.Setup(cfg.LogLevel)
	logger := logSetup.Logger
	slog.SetDefault(logger)

	var (
		templateStore store.TemplateStore
		manifestStore store.ManifestStore
		buildStore    store.BuildStore
		draftStore    store.DraftStore
		settingsStore store.SettingsStore
	)

	driver := strings.ToLower(strings.TrimSpace(cfg.DBDriver))
	switch driver {
	case "", "sqlite":
		sqliteStore, err := sqlite.New(cfg.DBURL)
		if err != nil {
			return nil, fmt.Errorf("init sqlite store: %w", err)
		}
		templateStore = sqliteStore
		manifestStore = sqliteStore
		buildStore = sqliteStore
		draftStore = sqliteStore
		settingsStore = sqliteStore
	default:
		memStore := memory.New()
		templateStore = memStore
		manifestStore = memStore
		buildStore = memStore
		draftStore = memStore
		settingsStore = memStore
	}

	qc := qcclient.New(cfg.QCBaseURL, cfg.QCToken, 15*time.Second, logger)
	templateSvc := templatesvc.New(qc, templateStore, manifestStore)
	onboardSvc := onboarding.New(qc, templateStore, manifestStore)
	draftSvc := draftsvc.New(draftStore, templateStore, manifestStore)
	mappingSvc := mapping.New()
	buildSvc := buildsvc.New(qc, templateStore, manifestStore, buildStore, mappingSvc, time.Duration(cfg.PollTimeoutSeconds)*time.Second)
	providerRuntime := llmruntime.NewFactory(45 * time.Second)
	suggestionGenerator := mapping.NewCompositeSuggestionGenerator(
		mapping.NewProviderAwareSuggestionGenerator(settingsStore, providerRuntime),
		mapping.NewCompositeSuggestionGenerator(
			mapping.NewQCLLMSuggestionGenerator(qc),
			mapping.NewRuleBasedSuggestionGenerator(),
		),
	)
	pollingSvc := polling.New(buildSvc, buildStore, time.Duration(cfg.PollIntervalSeconds)*time.Second, cfg.PollMaxConcurrent, logger)
	api := handlers.NewAPI(templateSvc, onboardSvc, draftSvc, buildSvc, logSetup.Recent)

	baseSettings := domain.AppSettings{
		QCBaseURL:              cfg.QCBaseURL,
		QCBearerTokenEncrypted: cfg.QCToken,
		LanguageOutputMode:     "EN",
		JobPollIntervalSeconds: cfg.PollIntervalSeconds,
		JobPollTimeoutSeconds:  cfg.PollTimeoutSeconds,
		LLMActiveProvider:      domain.DefaultLLMProvider(),
		LLMActiveModel:         domain.NormalizeLLMModel(domain.DefaultLLMProvider(), ""),
		LLMTemperature:         domain.DefaultLLMTemperature(),
		LLMMaxTokens:           domain.DefaultLLMMaxTokens(),
		MasterPrompt:           domain.SeedMasterPrompt,
		PromptBlocks:           domain.DefaultPromptBlocks(),
	}
	if existing, err := settingsStore.GetSettings(context.Background()); err == nil && existing != nil {
		baseSettings.LLMActiveProvider = existing.LLMActiveProvider
		baseSettings.LLMActiveModel = existing.LLMActiveModel
		baseSettings.LLMBaseURL = existing.LLMBaseURL
		baseSettings.LLMTemperature = existing.LLMTemperature
		baseSettings.LLMMaxTokens = existing.LLMMaxTokens
		baseSettings.OpenAIAPIKeyEncrypted = existing.OpenAIAPIKeyEncrypted
		baseSettings.AnthropicAPIKeyEncrypted = existing.AnthropicAPIKeyEncrypted
		baseSettings.GoogleAPIKeyEncrypted = existing.GoogleAPIKeyEncrypted
		baseSettings.XAIAPIKeyEncrypted = existing.XAIAPIKeyEncrypted
		baseSettings.OllamaAPIKeyEncrypted = existing.OllamaAPIKeyEncrypted
		baseSettings.MasterPrompt = existing.MasterPrompt
		baseSettings.PromptBlocks = existing.PromptBlocks
	}
	_ = settingsStore.UpsertSettings(context.Background(), baseSettings)

	renderer, err := views.NewRenderer("web/templates/*.gohtml")
	if err != nil {
		return nil, fmt.Errorf("init renderer: %w", err)
	}
	ui := handlers.NewUI(templateSvc, onboardSvc, draftSvc, buildSvc, settingsStore, suggestionGenerator, cfg, renderer, logger)

	server := httpserver.New(cfg.HTTPAddr, logger, func(r chi.Router) {
		r.Get("/", ui.Home)
		r.Get("/settings", ui.Settings)
		r.Post("/settings/llm", ui.SaveLLMSettings)
		r.Post("/settings/llm/validate", ui.ValidateLLMSettings)
		r.Post("/settings/prompt", ui.SavePromptSettings)
		r.Get("/templates", ui.Templates)
		r.Post("/templates/sync", ui.SyncTemplates)
		r.Get("/templates/{id}", ui.TemplateDetail)
		r.Post("/templates/{id}/onboard", ui.OnboardTemplate)
		r.Post("/templates/{id}/fields", ui.UpdateTemplateFields)
		r.Get("/builds/new", ui.BuildNew)
		r.Post("/builds/drafts", ui.SaveDraft)
		r.Post("/builds/drafts/autofill", ui.AutofillDraft)
		r.Post("/builds", ui.CreateBuild)
		r.Get("/builds/{id}", ui.BuildDetail)
		r.Post("/builds/{id}/poll", ui.PollBuild)
		r.Post("/builds/{id}/fetch-editor-url", ui.FetchEditorURL)

		r.Get("/healthz", api.Health)
		r.Route("/api", func(r chi.Router) {
			r.Post("/templates/sync", api.SyncTemplates)
			r.Get("/templates", api.ListTemplates)
			r.Get("/templates/{id}", api.GetTemplateDetail)
			r.Post("/templates/{id}/onboard", api.OnboardTemplate)
			r.Put("/templates/{id}/fields", api.UpdateTemplateFields)
			r.Post("/drafts/intake", api.IntakeDraft)
			r.Get("/drafts", api.ListDrafts)
			r.Get("/drafts/{id}", api.GetDraft)
			r.Put("/drafts/{id}", api.UpdateDraft)
			r.Post("/site-builds", api.StartBuild)
			r.Get("/site-builds/{id}", api.GetBuild)
			r.Post("/site-builds/{id}/poll", api.PollBuildOnce)
			r.Post("/site-builds/{id}/fetch-editor-url", api.FetchBuildEditorURL)
			r.Get("/logs", api.ListLogs)
		})
	})

	return &App{server: server, pollingSvc: pollingSvc}, nil
}

func (a *App) Run(ctx context.Context) error {
	go func() {
		if err := a.pollingSvc.Start(ctx); err != nil {
			// polling is best-effort in milestone-2; request flow works without supervisor
		}
	}()

	errCh := make(chan error, 1)
	go func() {
		if err := a.server.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			errCh <- fmt.Errorf("http run: %w", err)
		}
		close(errCh)
	}()

	select {
	case <-ctx.Done():
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		return a.server.Shutdown(shutdownCtx)
	case err := <-errCh:
		return err
	}
}
