package main

import (
	"database/sql"
	_ "embed"
	"encoding/json"
	"fmt"
	"os"
	"strings"

	_ "github.com/go-sql-driver/mysql"
)

const rutaConfig = "config.json"

// ConfigDB son los datos de conexión a MySQL. El nombre de la base de datos
// no va aquí: llega por el flag -db.
type ConfigDB struct {
	Usuario  string `json:"usuario"`
	Password string `json:"password"`
	Host     string `json:"host"`
}

type Config struct {
	DB ConfigDB `json:"db"`
	// Central es el nombre real de la base de datos central referenciada en los
	// .sql como CENTRAL. Si difiere, se sustituye en las queries antes de ejecutarlas.
	Central string `json:"central"`
	// LimiteMemoriaMB es el tope de memoria del proceso de exportación. Si se supera,
	// la exportación se cancela con ESTATUS=ERROR en lugar de agotar la RAM del servidor.
	// Ausente o por defecto = 1024 MB; 0 (o negativo) en config.json = sin límite.
	LimiteMemoriaMB int `json:"limite_memoria_mb"`
	// ClientesDir es el directorio padre donde viven las carpetas de archivos de
	// cliente; a él se le concatena {db}. En producción apunta a medis-demo-pro.
	// Ausente = "../medis-back/clientes" (checkout hermano de desarrollo).
	ClientesDir string `json:"clientes_dir"`
}

// cargarConfig lee config.json de la raíz. El archivo es obligatorio: si no
// existe o está malformado devuelve error. Los campos ausentes en el archivo
// conservan su valor por defecto (root sin contraseña en localhost:3306, central CENTRAL).
func cargarConfig() (Config, error) {
	cfg := Config{
		DB:              ConfigDB{Usuario: "root", Host: "localhost"},
		Central:         "CENTRAL",
		LimiteMemoriaMB: 1024, // default 1 GB si el campo está ausente en config.json
		ClientesDir:     "../medis-back/clientes",
	}
	data, err := os.ReadFile(rutaConfig)
	if err != nil {
		return Config{}, fmt.Errorf("no se pudo leer %s: %w", rutaConfig, err)
	}
	if err := json.Unmarshal(data, &cfg); err != nil {
		return Config{}, fmt.Errorf("error al parsear %s: %w", rutaConfig, err)
	}
	return cfg, nil
}

// dsn arma el DSN de MySQL para la base de datos indicada. El puerto es fijo (3306).
func (c ConfigDB) dsn(dbName string) string {
	return fmt.Sprintf("%s:%s@tcp(%s:3306)/%s", c.Usuario, c.Password, c.Host, dbName)
}

// aplicarCentral sustituye el prefijo de esquema "CENTRAL." por "{nombre}." en
// todas las queries embebidas. No hace nada si el nombre es vacío o "CENTRAL".
func aplicarCentral(nombre string) {
	if nombre == "" || nombre == "CENTRAL" {
		return
	}
	for k, q := range queries {
		queries[k] = strings.ReplaceAll(q, "CENTRAL.", nombre+".")
	}
}

//go:embed general.sql
var generalSQL string

//go:embed expedientes.sql
var expedientesSQL string

//go:embed caja.sql
var cajaSQL string

//go:embed alcance.sql
var alcanceSQL string

//go:embed agenda.sql
var agendaSQL string

var queries = initQueries()

func initQueries() map[string]string {
	m := parsearQueries(generalSQL)
	for k, v := range parsearQueries(expedientesSQL) {
		m[k] = v
	}
	for k, v := range parsearQueries(cajaSQL) {
		m[k] = v
	}
	for k, v := range parsearQueries(alcanceSQL) {
		m[k] = v
	}
	for k, v := range parsearQueries(agendaSQL) {
		m[k] = v
	}
	return m
}

func parsearQueries(contenido string) map[string]string {
	result := make(map[string]string)
	var nombre string
	var buf strings.Builder

	for _, linea := range strings.Split(contenido, "\n") {
		linea = strings.TrimRight(linea, "\r")
		if strings.HasPrefix(linea, "-- ") {
			if nombre != "" {
				if q := strings.TrimSpace(buf.String()); q != "" {
					result[nombre] = q
				}
			}
			nombre = strings.TrimPrefix(linea, "-- ")
			buf.Reset()
		} else if nombre != "" {
			buf.WriteString(linea + "\n")
		}
	}
	if nombre != "" {
		if q := strings.TrimSpace(buf.String()); q != "" {
			result[nombre] = q
		}
	}
	return result
}

func conectarDB(dsn string) (*sql.DB, error) {
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return nil, err
	}
	if err := db.Ping(); err != nil {
		db.Close()
		return nil, fmt.Errorf("error conectando a MySQL: %w", err)
	}
	return db, nil
}

// sanearTextoPDF reemplaza las runas fuera del plano básico (≥ U+10000, p. ej.
// emojis) por el carácter de reemplazo U+FFFD. La fuente del PDF (LiberationSerif)
// solo cubre el BMP; una runa fuera de rango deja a fpdf en estado de error
// permanente, que convierte AddPage en no-op y cuelga el render (ver drawDataRow).
func sanearTextoPDF(s string) string {
	return strings.Map(func(r rune) rune {
		if r >= 0x10000 {
			return '�'
		}
		return r
	}, s)
}

func ejecutarQuery(db *sql.DB, query string, args ...any) ([]string, []map[string]string) {
	logSQL(query, args...)
	rows, err := db.Query(query, args...)
	if err != nil {
		logFatal(err)
	}
	defer rows.Close()

	cols, err := rows.Columns()
	if err != nil {
		logFatal(err)
	}

	var result []map[string]string
	for rows.Next() {
		values := make([]any, len(cols))
		ptrs := make([]any, len(cols))
		for i := range values {
			ptrs[i] = &values[i]
		}
		if err := rows.Scan(ptrs...); err != nil {
			logFatal(err)
		}
		row := make(map[string]string, len(cols))
		for i, col := range cols {
			if values[i] == nil {
				row[col] = ""
			} else if b, ok := values[i].([]byte); ok {
				row[col] = sanearTextoPDF(string(b))
			} else {
				row[col] = sanearTextoPDF(fmt.Sprintf("%v", values[i]))
			}
		}
		result = append(result, row)
	}
	if err := rows.Err(); err != nil {
		logFatal(err)
	}
	return cols, result
}
