package pipeline

import (
	"context"
	"database/sql"
	"fmt"
	"os"
	"sync"
	"time"

	"gitlab.com/sistema-pro/xmlcolibex/internal/banco"
	"gitlab.com/sistema-pro/xmlcolibex/internal/configuracao"
	"gitlab.com/sistema-pro/xmlcolibex/internal/consulta"
	"gitlab.com/sistema-pro/xmlcolibex/internal/geracao"
	"gitlab.com/sistema-pro/xmlcolibex/internal/limpeza"
	"gitlab.com/sistema-pro/xmlcolibex/internal/log"
	"gitlab.com/sistema-pro/xmlcolibex/internal/portais"
	"gitlab.com/sistema-pro/xmlcolibex/internal/renderizacao"
)

type Executor struct {
	Cfg         *configuracao.Config
	Gerenciador *banco.GerenciadorConexao
	LogVisual   *log.Visual
	LogArquivo  *log.Arquivo
	// ForcarRegeneracao ignora a política diária e regera todos os feeds.
	ForcarRegeneracao bool
}

type ResultadoExecucao struct {
	ID             string
	Gerados        int
	Erros          int
	Ignorados      int
	RemovidosOrfaos int
	Duracao        time.Duration
}

func (e *Executor) ExecutarTudo(ctx context.Context) (ResultadoExecucao, error) {
	inicio := time.Now()
	id := inicio.Format("20060102-150405")
	res := ResultadoExecucao{ID: id}

	if err := e.Gerenciador.GarantirSaude(); err != nil {
		return res, err
	}

	if e.Cfg.ApagarTodosNoInicio {
		nApagados, err := limpeza.ApagarTodosXml(e.Cfg.SaidaXMLDir)
		if err != nil {
			return res, fmt.Errorf("limpeza: %w", err)
		}
		e.LogVisual.Apagados(nApagados, e.Cfg.SaidaXMLDir)
	}

	agora := time.Now()
	loc := e.Cfg.FusoGeracao

	var feeds []consulta.FeedConfig
	if err := e.Gerenciador.ExecutarComSaude(func(db *sql.DB) error {
		repo := consulta.NovoRepositorio(db)
		lista, err := repo.ListarFeedsAtivos(ctx, e.Cfg.OrdemMenorPrimeiro)
		if err != nil {
			return err
		}
		feeds = lista
		return nil
	}); err != nil {
		return res, fmt.Errorf("listar feeds: %w", err)
	}

	e.LogVisual.InicioExecucao(id, len(feeds), e.Cfg.Workers, e.Cfg.Turbo, e.ForcarRegeneracao, loc.String())

	type resultado struct {
		gerado   bool
		ignorado bool
	}
	resultados := make([]resultado, len(feeds))
	pool := e.Cfg.Workers
	if pool < 1 {
		pool = 1
	}
	jobs := make(chan int, len(feeds))
	for i := range feeds {
		jobs <- i
	}
	close(jobs)

	var mu sync.Mutex
	var wg sync.WaitGroup
	for w := 0; w < pool; w++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for idx := range jobs {
				if ctx.Err() != nil {
					return
				}
				feed := feeds[idx]
				if !e.ForcarRegeneracao {
					precisa, errCheck := geracao.PrecisaGerarHoje(e.Cfg.SaidaXMLDir, feed.Hash, agora, loc)
					if errCheck != nil {
						mu.Lock()
						e.LogVisual.FeedErro(idx+1, len(feeds), feed.IDXMLConfig, errCheck)
						e.LogArquivo.RegistrarErro(feed.IDXMLConfig, feed.Hash, errCheck)
						mu.Unlock()
						continue
					}
					if !precisa {
						mu.Lock()
						resultados[idx].ignorado = true
						e.LogVisual.FeedIgnorado(idx+1, len(feeds), feed.IDXMLConfig, feed.Hash, "já gerado hoje")
						mu.Unlock()
						continue
					}
				}
				err := e.processarFeed(ctx, idx+1, len(feeds), feed)
				mu.Lock()
				if err == nil {
					resultados[idx].gerado = true
				} else {
					e.LogVisual.FeedErro(idx+1, len(feeds), feed.IDXMLConfig, err)
					e.LogArquivo.RegistrarErro(feed.IDXMLConfig, feed.Hash, err)
				}
				mu.Unlock()
			}
		}()
	}
	wg.Wait()

	for _, r := range resultados {
		switch {
		case r.gerado:
			res.Gerados++
		case r.ignorado:
			res.Ignorados++
		default:
			res.Erros++
		}
	}
	if e.Cfg.LimparOrfaosNoFim {
		hashes := make([]string, 0, len(feeds))
		for _, f := range feeds {
			hashes = append(hashes, f.Hash)
		}
		nOrfaos, err := limpeza.RemoverXmlOrfaos(e.Cfg.SaidaXMLDir, limpeza.ConjuntoHashes(hashes))
		if err != nil {
			return res, fmt.Errorf("limpar órfãos: %w", err)
		}
		res.RemovidosOrfaos = nOrfaos
		if nOrfaos > 0 {
			e.LogVisual.OrfaosRemovidos(nOrfaos, e.Cfg.SaidaXMLDir)
		}
	}

	res.Duracao = time.Since(inicio)

	e.LogVisual.Fim(res.Gerados, res.Erros, res.Ignorados, res.RemovidosOrfaos, res.Duracao)
	_ = e.LogArquivo.SalvarResumo(log.ResumoExecucao{
		ID:              id,
		Inicio:          inicio,
		Fim:             time.Now(),
		DuracaoSeg:      res.Duracao.Seconds(),
		Gerados:         res.Gerados,
		Erros:           res.Erros,
		Ignorados:       res.Ignorados,
		RemovidosOrfaos: res.RemovidosOrfaos,
		TotalFeeds:      len(feeds),
	})
	return res, nil
}

func (e *Executor) ExecutarFeed(ctx context.Context, hash string) error {
	var feed *consulta.FeedConfig
	if err := e.Gerenciador.ExecutarComSaude(func(db *sql.DB) error {
		repo := consulta.NovoRepositorio(db)
		f, err := repo.BuscarFeedPorHash(ctx, hash)
		if err != nil {
			return err
		}
		feed = f
		return nil
	}); err != nil {
		return err
	}
	return e.processarFeed(ctx, 1, 1, *feed)
}

func (e *Executor) processarFeed(ctx context.Context, idx, total int, feed consulta.FeedConfig) error {
	portalNome := portais.NomePortal(feed.IDPortal)
	e.LogVisual.FeedInicio(idx, total, feed.IDXMLConfig, feed.Hash, portalNome, feed.IDMaster)

	motor := renderizacao.NovoMotor(e.Cfg.ModelosDir)
	var xml []byte

	err := e.Gerenciador.ExecutarComSaude(func(db *sql.DB) error {
		if err := db.PingContext(ctx); err != nil {
			if recErr := e.Gerenciador.Reconectar(); recErr != nil {
				return recErr
			}
			e.LogVisual.ReconexaoMySQL()
			if err := db.PingContext(ctx); err != nil {
				return err
			}
		}
		ger := &portais.Gerador{
			Cfg:   e.Cfg,
			Repo:  consulta.NovoRepositorio(db),
			Motor: motor,
		}
		var genErr error
		xml, genErr = ger.GerarFeed(ctx, feed, func(feito, tot int, msg string) {
			if tot > 0 && (feito == tot || feito%500 == 0 || feito == 0) {
				e.LogVisual.ImoveisProgresso(feed.IDXMLConfig, feito, tot, msg)
			}
		})
		return genErr
	})
	if err != nil {
		return err
	}

	path, err := renderizacao.GravarAtomico(e.Cfg.SaidaXMLDir, feed.Hash, xml)
	if err != nil {
		return err
	}
	tamanho := tamanhoArquivoMB(path)
	url := e.Cfg.URLPublicaXML
	e.LogVisual.FeedOK(feed.IDXMLConfig, feed.Hash, tamanho, url)
	return nil
}

func tamanhoArquivoMB(path string) float64 {
	fi, err := os.Stat(path)
	if err != nil {
		return 0
	}
	return float64(fi.Size()) / (1024 * 1024)
}
