package database

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"

	"consumer/internal/logging"
	"consumer/internal/models/entidad_db"
)

// Constantes y errores
const dbQueryTimeout = 5 * time.Second

var (
	// Custom errors
	ErrInvalidUUID      = errors.New("ID de transacción inválido")
	ErrInvalidMessage   = errors.New("mensaje inválido")
	ErrCompanyNotFound  = errors.New("empresa no encontrada")
	ErrAlreadyProcessed = errors.New("factura ya procesada y enviada a DLQ")
	ErrNilPointer       = errors.New("puntero nulo encontrado en los datos")
)

// DBManager maneja las operaciones de base de datos
type DBManager struct {
	db     *sql.DB
	logger *logging.LoggerSystem
}

// NewDBManager crea un nuevo gestor de base de datos
func NewDBManager(dsn string, logger *logging.LoggerSystem) (*DBManager, error) {
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		return nil, fmt.Errorf("error abriendo conexión a base de datos: %w", err)
	}

	// Configure connection pool
	db.SetMaxOpenConns(20)
	db.SetMaxIdleConns(10)
	db.SetConnMaxLifetime(time.Hour)

	// Test connection
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := db.PingContext(ctx); err != nil {
		return nil, fmt.Errorf("error pingueando base de datos: %w", err)
	}

	return &DBManager{
		db:     db,
		logger: logger,
	}, nil
}

// Close cierra la conexión a la base de datos
func (dm *DBManager) Close() error {
	return dm.db.Close()
}

// isFacturaInDLQ comprueba si una factura ya fue procesada y enviada a DLQ
func (dm *DBManager) IsFacturaInDLQ(ctx context.Context, id string) (bool, error) {
	var exists bool
	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	err := dm.db.QueryRowContext(ctx,
		"SELECT EXISTS(SELECT 1 FROM facturacion_dlq WHERE transaccion_id = $1)",
		id).Scan(&exists)

	if err != nil {
		return false, fmt.Errorf("error verificando DLQ: %w", err)
	}

	return exists, nil
}

// getDatosFacturaEmpresa obtiene los datos de registro de empresa necesarios para la factura
func (dm *DBManager) GetDatosFacturaEmpresa(
	ctx context.Context,
	nit int64,
	codigoSucursal int,
	codigoPuntoVenta int,
) (entidad_db.DatosFactura, error) {
	var datosFactura entidad_db.DatosFactura

	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	const queryEmpresa = `
SELECT re.id, re.codigo_ambiente, re.codigo_modalidad, re.codigo_punto_venta,
       re.codigo_sistema, re.codigo_sucursal, re.nit,
       cu.cuis, cf.codigo, re.token_key, re.token_value, re.nombre_archivo_certificado, re.nombre_archivo_clave_privada
FROM registroEmpresa re
JOIN cuis cu ON cu.registro_empresa_id = re.id
JOIN cufd cf ON cf.cuis_id = cu.id
WHERE re.nit = $1 AND re.codigo_sucursal = $2 AND re.codigo_punto_venta = $3
ORDER BY cu.fecha_vigencia DESC, cf.fecha_vigencia DESC
LIMIT 1`

	if err := dm.db.QueryRowContext(ctx, queryEmpresa,
		nit, codigoSucursal, codigoPuntoVenta,
	).Scan(
		&datosFactura.RegistroEmpresaID,
		&datosFactura.CodigoAmbiente,
		&datosFactura.CodigoModalidad,
		&datosFactura.CodigoPuntoVenta,
		&datosFactura.CodigoSistema,
		&datosFactura.CodigoSucursal,
		&datosFactura.NIT,
		&datosFactura.CUIS,
		&datosFactura.CUFD,
		&datosFactura.TokenKey,
		&datosFactura.TokenValue,
		&datosFactura.NombreArchivoCertificado,
		&datosFactura.NombreArchivoClavePrivada,
	); err != nil {
		if err == sql.ErrNoRows {
			return datosFactura, ErrCompanyNotFound
		}
		return datosFactura, fmt.Errorf("error buscando registroEmpresa: %w", err)
	}

	return datosFactura, nil
}

// getCUISDatosFactura obtiene el CUIS y datos relacionados
func (dm *DBManager) GetCUISDatosFactura(ctx context.Context, registroEmpresaID int) (int, string, string, error) {
	var cuisID int
	var cufd, codControl string

	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	// Obtener CUIS ID
	if err := dm.db.QueryRowContext(ctx,
		`SELECT id FROM cuis WHERE registro_empresa_id = $1 ORDER BY fecha_vigencia DESC LIMIT 1`,
		registroEmpresaID,
	).Scan(&cuisID); err != nil {
		return 0, "", "", fmt.Errorf("error buscando cuisID: %w", err)
	}

	// Obtener CUFD y código de control
	if err := dm.db.QueryRowContext(ctx,
		"SELECT codigo, codigo_control FROM cufd WHERE cuis_id = $1",
		cuisID,
	).Scan(&cufd, &codControl); err != nil {
		return cuisID, "", "", fmt.Errorf("error buscando cufd: %w", err)
	}

	return cuisID, cufd, codControl, nil
}

// verificarExistenciaFactura verifica si una factura existe y obtiene sus datos
func (dm *DBManager) VerificarExistenciaFactura(ctx context.Context, id string) (
	bool, string, string, time.Time, error) {

	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	var exists bool
	if err := dm.db.QueryRowContext(ctx,
		"SELECT EXISTS(SELECT 1 FROM facturacion_facturas WHERE id=$1)",
		id,
	).Scan(&exists); err != nil {
		return false, "", "", time.Time{}, fmt.Errorf("error verificando existencia: %w", err)
	}

	if !exists {
		return false, "", "", time.Time{}, nil
	}

	var numeroFactura, estado string
	var fechaEmision time.Time

	err := dm.db.QueryRowContext(ctx, `
SELECT fecha_emision, estado
FROM facturacion_facturas WHERE id = $1`, id,
	).Scan(&fechaEmision, &estado)

	if err != nil {
		return true, "", "", time.Time{}, fmt.Errorf("error obteniendo datos: %w", err)
	}

	return true, numeroFactura, estado, fechaEmision, nil
}

// registra una interacción con el servicio SOAP
func (dm *DBManager) RegistrarInteraccionServicio(ctx context.Context, id, tipoServicio, endpoint, reqBody, respBody string,
	statusCode int, duracion int64, exitoso bool, vMsgRespSoap string) error {

	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	_, err := dm.db.ExecContext(ctx, `
INSERT INTO facturacion_servicio_interacciones
  (factura_id, tipo_servicio, endpoint, request_body, response_body, status_code, duracion_ms, exitoso, msg_soap)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
		id, tipoServicio, endpoint, reqBody, respBody, statusCode, duracion, exitoso, vMsgRespSoap)

	if err != nil {
		return fmt.Errorf("error registrando interacción: %w", err)
	}

	return nil
}

// crea un registro de factura pendiente
func (dm *DBManager) CrearFacturaPendiente(ctx context.Context, id string,
	fechaEmision time.Time, estado string, vCuf string, vUrlFact string, vEmpresaID int, vCodDocSector int, vFactura string) error {

	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	_, err := dm.db.ExecContext(ctx, `
INSERT INTO facturacion_facturas
  (id, fecha_emision, estado, cuf, url, registro_empresa_id, codigo_documento_sector, factura)
VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, id, fechaEmision, estado, vCuf, vUrlFact, vEmpresaID, vCodDocSector, vFactura)

	if err != nil {
		return fmt.Errorf("error creando factura pendiente: %w", err)
	}

	return nil
}

// obtiene el CUIS y datos relacionados
func (dm *DBManager) GetFacturacionFacturas(ctx context.Context, id_transaccion string) (string, int, string, error) {
	var vRegEmpresaID int
	var vCuf string
	var vEstado string

	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	// Obtenemos el ID del registro de la empresa
	if err := dm.db.QueryRowContext(ctx,
		`SELECT cuf, registro_empresa_id, estado FROM facturacion_facturas WHERE id = $1`, id_transaccion,
	).Scan(&vCuf, &vRegEmpresaID, &vEstado); err != nil {
		return "", 0, "", fmt.Errorf("error al buscar registro_empresa_id: %w", err)
	}

	return vCuf, vRegEmpresaID, vEstado, nil
}

// Obtiene los datos de la tabla RegistroEmpresa, cui, cufd
func (dm *DBManager) GetRegEmpresaCuisCuf(ctx context.Context, idRegCompra int) (entidad_db.DatosFactura, error) {
	var datosFactura entidad_db.DatosFactura

	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	const queryEmpresa = `
SELECT re.id, re.codigo_ambiente, re.codigo_modalidad, re.codigo_punto_venta,
       re.codigo_sistema, re.codigo_sucursal, re.nit,
       cu.cuis, cf.codigo, cf.codigo_control, re.token_key, re.token_value, re.nombre_archivo_clave_privada, re.nombre_archivo_certificado
FROM registroEmpresa re
JOIN cuis cu ON cu.registro_empresa_id = re.id
JOIN cufd cf ON cf.cuis_id = cu.id
WHERE re.id = $1 ORDER BY cu.fecha_vigencia DESC, cf.fecha_vigencia DESC LIMIT 1`

	if err := dm.db.QueryRowContext(
		ctx, queryEmpresa, idRegCompra,
	).Scan(
		&datosFactura.RegistroEmpresaID,
		&datosFactura.CodigoAmbiente,
		&datosFactura.CodigoModalidad,
		&datosFactura.CodigoPuntoVenta,
		&datosFactura.CodigoSistema,
		&datosFactura.CodigoSucursal,
		&datosFactura.NIT,
		&datosFactura.CUIS,
		&datosFactura.CUFD,
		&datosFactura.CodigoControl,
		&datosFactura.TokenKey,
		&datosFactura.TokenValue,
		&datosFactura.NombreArchivoClavePrivada,
		&datosFactura.NombreArchivoCertificado,
	); err != nil {
		if err == sql.ErrNoRows {
			return datosFactura, ErrCompanyNotFound
		}
		return datosFactura, fmt.Errorf("error buscando tablas registroEmpresa, cuis, cufd: %w", err)
	}

	return datosFactura, nil
}

// actualizarEstadoFactura actualiza el estado de una factura
func (dm *DBManager) ActualizarEstadoFactura(ctx context.Context, id, nuevoEstado, codigoAutorizacion string,
	estadoAnterior string, detalles string, vFacturaFirmada string) error {

	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	tx, err := dm.db.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("error iniciando transacción: %w", err)
	}

	defer func() {
		if err != nil {
			tx.Rollback()
		}
	}()

	var updateQuery string
	var params []interface{}

	if codigoAutorizacion != "" {
		updateQuery = `UPDATE facturacion_facturas SET estado=$1, codigo_autorizacion=$2 WHERE id=$3`
		params = []interface{}{nuevoEstado, codigoAutorizacion, id}
	} else {
		if vFacturaFirmada != "" {
			updateQuery = `UPDATE facturacion_facturas SET estado=$1, factura_firmada=$2 WHERE id=$3`
			params = []interface{}{nuevoEstado, vFacturaFirmada, id}
		} else {
			updateQuery = `UPDATE facturacion_facturas SET estado=$1 WHERE id=$2`
			params = []interface{}{nuevoEstado, id}
		}
	}

	if _, err = tx.ExecContext(ctx, updateQuery, params...); err != nil {
		return fmt.Errorf("error actualizando estado: %w", err)
	}

	if _, err = tx.ExecContext(ctx, `
INSERT INTO facturacion_eventos_factura
  (factura_id, estado_anterior, estado_nuevo, detalles)
VALUES($1,$2,$3,$4)`,
		id, estadoAnterior, nuevoEstado, detalles); err != nil {
		return fmt.Errorf("error registrando evento: %w", err)
	}

	if err = tx.Commit(); err != nil {
		return fmt.Errorf("error haciendo commit: %w", err)
	}

	return nil
}

// inserta un mensaje en la tabla de DLQ
func (dm *DBManager) InsertarEnDLQ(ctx context.Context, id string, mensaje string, estadoSoap string) error {
	ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
	defer cancel()

	// Verificar si ya existe
	var exists bool
	if err := dm.db.QueryRowContext(ctx,
		"SELECT EXISTS(SELECT 1 FROM facturacion_dlq WHERE transaccion_id = $1)",
		id).Scan(&exists); err != nil {
		return fmt.Errorf("error verificando DLQ: %w", err)
	}

	// Si ya existe, no insertar nuevamente
	if exists {
		return nil
	}

	_, err := dm.db.ExecContext(ctx,
		"INSERT INTO facturacion_dlq(transaccion_id, mensaje, estado) VALUES($1,$2,$3)",
		id, mensaje, estadoSoap)

	if err != nil {
		return fmt.Errorf("error insertando en DLQ: %w", err)
	}

	return nil
}
