package infrastructure.adapter.input.rest;

import application.factory.SecurityTraceFactory;
import application.helper.ResponseHelper;
import application.port.input.*;
import application.port.input.command.*;
import application.service.OtpValidationService;
import application.service.PostgresConfigService;
import com.banesco.common.infraestructure.helpers.JsonHelper;
import com.banesco.common.infraestructure.helpers.LoggerHelper;
import domain.dto.request.CustomerRequest;
import domain.dto.response.CustomerResponse;
import domain.dto.response.StatusResponse;
import domain.entity.AppConfiguration;
import domain.entity.ConfigItem;
import domain.model.AccountClientContext;
import domain.model.BankService;
import domain.model.ErrorCode;
import domain.model.UserLogin;
import infrastructure.config.AvailabilityBolConfig;
import infrastructure.context.TraceIdContext;
import infrastructure.service.StatusDescriptionResolver;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.ExampleObject;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameters;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;

import java.util.Map;
import java.util.Optional;

@Slf4j
@Path("/customer")
public class CustomerResource {

    @ConfigProperty(name = "app.redis.expiration")
    long cacheTtl;

    // --- DEPENDENCIAS INYECTADAS ---
    private final TraceIdContext traceIdContext;
    private final ResponseHelper responseHelper;
    private final SecurityTraceFactory securityTraceFactory;
    private final StatusDescriptionResolver statusDescriptionResolver;

    // --- CONFIGURACIONES ---
    private final AvailabilityBolConfig bolConfig;

    // --- CASOS DE USO Y SERVICIOS ---
    private final IAppConfig appConfigUseCase;
    private final IServiceStatus serviceStatusUseCase;
    private final IUserLogin userLoginUseCase;
    private final IValidateProfile validateProfileUseCase;
    private final IEncodeValue encodeValueUseCase;
    private final ICustomerProduct customerProductUseCase;
    private final IRegisterSecurity registerSecurityUseCase;
    private final OtpValidationService otpValidationService;
    private final PostgresConfigService postgresConfigService;

    public CustomerResource(
            TraceIdContext traceIdContext,
            ResponseHelper responseHelper,
            SecurityTraceFactory securityTraceFactory,
            AvailabilityBolConfig bolConfig,
            StatusDescriptionResolver statusDescriptionResolver,
            IAppConfig appConfigUseCase,
            IServiceStatus serviceStatusUseCase,
            IUserLogin userLoginUseCase,
            IValidateProfile validateProfileUseCase,
            IEncodeValue encodeValueUseCase,
            ICustomerProduct customerProductUseCase,
            IRegisterSecurity registerSecurityUseCase,
            OtpValidationService otpValidationService,
            PostgresConfigService postgresConfigService
    ) {
        this.traceIdContext = traceIdContext;
        this.responseHelper = responseHelper;
        this.securityTraceFactory = securityTraceFactory;
        this.bolConfig = bolConfig;
        this.statusDescriptionResolver = statusDescriptionResolver;
        this.appConfigUseCase = appConfigUseCase;
        this.serviceStatusUseCase = serviceStatusUseCase;
        this.userLoginUseCase = userLoginUseCase;
        this.validateProfileUseCase = validateProfileUseCase;
        this.encodeValueUseCase = encodeValueUseCase;
        this.customerProductUseCase = customerProductUseCase;
        this.registerSecurityUseCase = registerSecurityUseCase;
        this.otpValidationService = otpValidationService;
        this.postgresConfigService = postgresConfigService;
    }

    @POST
    @Path("/position/evaluate")
    @Operation(
            summary = "Consulta de cuentas",
            description = "Orquesta el flujo de validaciones (estatus de servicio, perfil, OTP, etc.) y, si son exitosas, consulta los productos del cliente."
    )
    @Parameters({
            @Parameter(
                    name = "requestId",
                    in = ParameterIn.HEADER,
                    description = "Identificador de la solicitud para la trazabilidad. Si se omite, se genera uno automático.",
                    schema = @Schema(type = SchemaType.STRING),
                    example = "7WCnvznd0CPrBZ0jF4af"
            ),
            @Parameter(
                    name = "appId",
                    in = ParameterIn.HEADER,
                    description = "Identificador de la aplicación cliente para cargar una configuración de header específica. Si se omite, se usa la configuración por defecto.",
                    schema = @Schema(type = SchemaType.STRING),
                    example = "abc123"
            )
    })
    @RequestBody(
            description = "Cuerpo de la solicitud con los datos necesarios para consultar las cuentas.",
            content = @Content(mediaType = MediaType.APPLICATION_JSON, schema =
            @Schema(implementation = CustomerRequest.class),
                    examples = {
                            @ExampleObject(
                                    name = "Solicitud Estándar",
                                    summary = "Ejemplo de solicitud con éxito",
                                    externalValue = "/openapi-examples/request/customer-product-success.json"
                            ),
                            @ExampleObject(
                                    name = "Solicitud con Token",
                                    summary = "Ejemplo de solicitud con token (éxito)",
                                    externalValue = "/openapi-examples/request/customer-product-token-success.json"
                            )
                    }
            )
    )
    @APIResponses(value = {
            @APIResponse(
                    responseCode = "200",
                    description = "Operación Exitosa. Devuelve la lista de productos del cliente.",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON,
                            schema = @Schema(implementation = CustomerResponse.class),
                            examples = {
                                    @ExampleObject(
                                            name = "Cuentas Enmascaradas",
                                            summary = "Respuesta exitosa (Enmascaramiento activo)",
                                            description = "La configuración de la app indica que 'encryptionData' es true.",
                                            externalValue = "/openapi-examples/response/customer-product-response.json"
                                    ),
                                    @ExampleObject(
                                            name = "Cuentas Reales",
                                            summary = "Respuesta exitosa (Sin enmascaramiento)",
                                            description = "La configuración de la app indica que 'encryptionData' es false.",
                                            externalValue = "/openapi-examples/response/customer-product-response-unmasked.json"
                                    ),
                                    @ExampleObject(
                                            name = "Sin Resultados",
                                            summary = "Respuesta exitosa (Cliente sin productos)",
                                            description = "La consulta se completó, pero el cliente no tiene productos que coincidan.",
                                            externalValue = "/openapi-examples/response/customer-product-without-results.json"
                                    )
                            }
                    )
            ),
            @APIResponse(
                    responseCode = "401",
                    description = "Error de Autorización o Perfil. El perfil del cliente no es válido o falló una validación de seguridad.",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON,
                            schema = @Schema(implementation = StatusResponse.class),
                            examples = {
                                    @ExampleObject(
                                            name = "Perfil Inválido",
                                            summary = "VRN01 - Perfil no válido",
                                            externalValue = "/openapi-examples/response/invalid-profile.json"
                                    )
                            }
                    )
            ),
            @APIResponse(
                    responseCode = "400",
                    description = "Datos de entrada inválidos, como un token OTP incorrecto o una configuración de app no encontrada.",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON,
                            schema = @Schema(implementation = StatusResponse.class),
                            examples = {
                                    @ExampleObject(
                                            name = "Token Inválido",
                                            summary = "VRN07 - OTP Inválido",
                                            externalValue = "/openapi-examples/response/invalid-token.json"
                                    ),
                                    @ExampleObject(
                                            name = "Configuración no encontrada",
                                            summary = "VRN_CONFIG_NOT_FOUND",
                                            externalValue = "/openapi-examples/response/config-not-found.json"
                                    )
                            }
                    )
            ),
            @APIResponse(
                    responseCode = "404",
                    description = "Recurso no encontrado. Puede ser un usuario BOL, un login vacío.",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON,
                            schema = @Schema(implementation = CustomerResponse.class),
                            examples = {
                                    @ExampleObject(
                                            name = "Usuario no existe",
                                            summary = "AUTH02 - Usuario no existe",
                                            externalValue = "/openapi-examples/response/user-bol-not-exist.json"
                                    ),
                                    @ExampleObject(
                                            name = "Usuario vacío",
                                            summary = "AUTH01 - Login vacío",
                                            externalValue = "/openapi-examples/response/user-not-exist.json"
                                    ),
                            }
                    )
            ),
            @APIResponse(
                    responseCode = "409",
                    description = "CONFLICTO.",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON,
                            schema = @Schema(implementation = StatusResponse.class),
                            examples = {
                                    @ExampleObject(
                                            name = "Conflicto",
                                            summary = "Conflicto (revisar)",
                                            externalValue = "/openapi-examples/response/conflict.json"
                                    )
                            }
                    )
            ),
            @APIResponse(
                    responseCode = "500",
                    description = "Uso Interno. Ocurrio un error inesperado.",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON,
                            schema = @Schema(implementation = StatusResponse.class),
                            examples = {
                                    @ExampleObject(
                                            name = "Uso Interno",
                                            summary = "Uso Interno (revisar)",
                                            externalValue = "/openapi-examples/response/internal-use.json"
                                    )
                            }
                    )
            ),
            @APIResponse(
                    responseCode = "503",
                    description = "Servicio no disponible. Ocurrió un error con una dependencia interna o el servicio está en mantenimiento.",
                    content = @Content(
                            mediaType = MediaType.APPLICATION_JSON,
                            schema = @Schema(implementation = StatusResponse.class),
                            examples = {
                                    @ExampleObject(
                                            name = "Mantenimiento",
                                            summary = "VRN04 - Servicio en mantenimiento",
                                            externalValue = "/openapi-examples/response/service-unavailable.json"
                                    ),
                                    @ExampleObject(
                                            name = "Error de Dependencia",
                                            summary = "Error de CI",
                                            externalValue = "/openapi-examples/response/customer-product-rest-client-error.json"
                                    )
                            }
                    )
            )
    })
    public Response getCustomerProducts(
            @HeaderParam("appId") String appId,
            @HeaderParam("customerReferenceFintechId") String customerReferenceFintechId,
            @HeaderParam("customerReferenceUser") String customerReferenceUser,
            @Valid @NotNull(message = "VDE01") CustomerRequest request
            ) {
        final var requestId = traceIdContext.getTraceId();
        final long startExecution = System.currentTimeMillis();

        log.info(LoggerHelper.buildInfoPrivateRequest(requestId, JsonHelper.getJsonFromObject(request)));

        var context = new AccountClientContext(startExecution, requestId, request,
                appId, customerReferenceFintechId, customerReferenceUser
        );

        var appConfigurationOpt = getPgConfig(context, requestId);

        if (appConfigurationOpt.isEmpty()) {
            var error = statusDescriptionResolver.getCode("VRN_CONFIG_NOT_FOUND");
            return responseHelper.buildErrorResponse(error, requestId);
        }

        var appConfiguration = appConfigurationOpt.get();

        var validationRules = appConfiguration.getConfiguration().getValidationRules();
        var validationErrorOpt = runValidationPipeline(context, validationRules, requestId);

        Response finalResponse;
        if (validationErrorOpt.isPresent()) {
            context.setFinalError(validationErrorOpt);
            finalResponse = responseHelper.buildErrorResponse(validationErrorOpt.get(), requestId);
        } else {
            var configItem = findSpecificConfigItem(appConfiguration, context, requestId);

            if (configItem == null) {
                log.error(LoggerHelper.buildError(requestId, "FATAL: No se encontro un ConfigItem (ni especifico ni 'all')"));
                var error = statusDescriptionResolver.getCode("VRN_ITEM_NOT_FOUND");
                return responseHelper.buildErrorResponse(error, requestId);
            }

            var otp = Optional.ofNullable(request.getTokenAssignmentInstance())
                    .map(CustomerRequest.TokenAssignmentInstance::getTokenIdentificationCode)
                    .filter(otpCode -> !otpCode.isEmpty())
                    .orElse(null);

            finalResponse = getUserAccounts(context, appConfiguration, configItem, otp, requestId);
        }

        var fullResponse = (CustomerResponse) finalResponse.getEntity();

        var statusRegister = fullResponse.getStatusResponse();

        log.info(LoggerHelper.buildInfoPrivateRequest(requestId, "Ejecutando tareas finales (traza de seguridad)"));
        var securityCommand = securityTraceFactory.createCommandFromContext(context, statusRegister);
        registerSecurityUseCase.execute(securityCommand, context.getRequestId());

        return finalResponse;
    }

    /**
     * Ejecuta la secuencia de validaciones. Si alguna falla, devuelve la respuesta de error.
     * Si todas pasan, devuelve un Optional vacío.
     */
    @SuppressWarnings("t")
    private Optional<ErrorCode> runValidationPipeline(AccountClientContext context, Map<String, Boolean> validationRules, String requestId) {
        var data = context.getRequest();
        var device = data.getDevice();
        var customerReference = data.getCustomerPositionState().getCustomerReference();
        var tokenInstance = data.getTokenAssignmentInstance();

        if (shouldRun(validationRules, "serviceStatus")) {
            if (!isServiceActive(context.getRequestId())) {
                return Optional.of(statusDescriptionResolver.getCode("VRN04"));
            }
        }

        // Primero intenta obtener el usuario del contexto
        var userOpt = Optional.ofNullable(context.getUsername())
                .filter(u -> !u.isBlank())
                .map(u -> UserLogin.builder()
                        .usernameBol(u)
                        .idNumber(customerReference.getCustomerId())
                        .build());

        // Si no está en el contexto Y la regla dice que se debe ejecutar, búscalo en la API
        if (shouldRun(validationRules, "getUserLogin")) {
            userOpt = getUserLogin(context.getRequestId(), customerReference.getCustomerId(), customerReference.getCustomerIdType());

            if (userOpt.isEmpty() ) {
                log.info(LoggerHelper.buildInfoPrivateResponse(requestId, "No se pudo obtener el usuario ni desde el contexto ni desde la API"));
                return Optional.of(statusDescriptionResolver.getCode("VRN19"));
            }

            // Si viene vacio el usernameBol ""
            if(userOpt.get().usernameBol().isBlank()) {
                log.info(LoggerHelper.buildInfoPrivateResponse(requestId, "No se pudo obtener el usernameBol"));
                return Optional.of(statusDescriptionResolver.getCode("VRN20"));
            }

            context.setUsername(userOpt.get().usernameBol());
        }

        String hash = ""; // Inicializar
        if (shouldRun(validationRules, "encodeValue")) {
            hash = encodeUsername(context.getUsername(), requestId);
        }

        if (shouldRun(validationRules, "validateOtp")) {
            if(!tokenInstance.getTokenIdentificationCode().isEmpty()) {
                var isOtpValid = otpValidationService.isOtpValid(
                        tokenInstance.getTokenIdentificationCode(),
                        context.getFintechId(),
                        context.getUsername(),
                        device.getDeviceIp(),
                        requestId
                );
                if (!isOtpValid) {
                    return Optional.of(statusDescriptionResolver.getCode("VRN07"));
                }
            }
        }

        if (shouldRun(validationRules, "validateProfile")) {
            var command = new ValidateProfileCommand(
                    device.getDeviceSessionReference() == null || device.getDeviceSessionReference().isBlank() ? context.getFinalCustomerId() + device.getDeviceSessionReference() : context.getFinalCustomerId(),
                    device.getDeviceIp(),
                    hash
            );
            var isValidProfile = validateProfileUseCase.execute(command, requestId);

            if(!isValidProfile) {
                log.info(LoggerHelper.buildInfoPrivateResponse(requestId, "FALLO LA VALIDACION DEL CLIENTE EN TRANSACT"));
                return Optional.of(statusDescriptionResolver.getCode("VRN01"));
            }
        }

        return Optional.empty();

    }

    // --- MÉTODOS PRIVADOS DE ORQUESTACIÓN ---
    private boolean isServiceActive(String requestId) {
        var bankService = new BankService(bolConfig.codBan(), bolConfig.codEve(), bolConfig.codServ());
        var command = new ServiceStatusCommand(requestId, requestId, bankService);
        return serviceStatusUseCase.execute(command, requestId);
    }

    private Optional<UserLogin> getUserLogin(String requestId, String... params) {
        var command = new UserLoginCommand(
            params[0],
            params[1]
        );

        return userLoginUseCase.execute(command, requestId);
    }

    private String encodeUsername(String username, String requestId) {
        var command = new EncodeValueCommand(username);
        return encodeValueUseCase.execute(command, requestId);
    }

    private Optional<AppConfiguration> getPgConfig(AccountClientContext context, String requestId) {
        var configKey = context.getAppId();

        var cachedConfig = postgresConfigService.findByName(configKey, requestId);
        if (cachedConfig.isPresent()) {
            log.info(LoggerHelper.buildInfoPrivateRequest(requestId, "Configuracion obtenida desde CACHE"));
            return cachedConfig;
        }

        log.info(LoggerHelper.buildInfoPrivateRequest(requestId, "Configuracion no encontrada en cache, buscando en BD..."));

        AppConfiguration appConfiguration;
        try {
            appConfiguration = appConfigUseCase.execute(configKey);

            log.info(LoggerHelper.buildInfoPrivateResponse(requestId, "Configuracion obtenida desde BD"));
        } catch (NotFoundException ex) {
            log.error(LoggerHelper.buildError(requestId, "FATAL: No se encontro AppConfiguration para appId: " + configKey), ex);
            return Optional.empty();
        }

        postgresConfigService.savePostgresConfig(configKey, appConfiguration, cacheTtl, requestId);
        return Optional.of(appConfiguration);
    }

    /**
     * Extrae el ConfigItem específico de la AppConfiguration basado en el contexto.
     * Cae a "all" si no se encuentra uno específico.
     */
    private ConfigItem findSpecificConfigItem(AppConfiguration appConfiguration, AccountClientContext context, String requestId) {
        var data = context.getRequest().getCustomerPositionState().getCustomerReference();
        var postgresConfig = appConfiguration.getConfiguration();

        var aliases = postgresConfig.getAliases();
        var configMap = postgresConfig.getConfiguration();

        // Usamos el appId del contexto como prefijo de la clave
        var literal = data.getCustomerIdType();
        var finalLiteral = aliases.getOrDefault(literal, literal);
        var specificKey = context.getAppId() + "_" + finalLiteral;

        log.info(LoggerHelper.buildInfoPrivateRequest(requestId, "Buscando ConfigItem con KEY: " + specificKey));

        ConfigItem config;
        if (configMap.containsKey(specificKey)) {
            config = configMap.get(specificKey);
            log.info(LoggerHelper.buildInfoPrivateRequest(requestId, "Configuracion encontrada (Especifica): " + specificKey));
        } else {
            config = configMap.get("all");
            log.info(LoggerHelper.buildInfoPrivateRequest(requestId, "Configuracion encontrada (General): all"));
        }

        return config; // Puede ser null si "all" tampoco existe
    }

    /**
     * Helper para verificar de forma segura si una regla debe ejecutarse.
     * Por defecto, si una regla no está en el mapa, SÍ se ejecuta (falla seguro).
     */
    private boolean shouldRun(Map<String, Boolean> rules, String ruleName) {
        if (rules == null) {
            // Si no hay mapa, orquesta completo
            return true;
        }
        return rules.getOrDefault(ruleName, true);
    }


    private Response getUserAccounts(AccountClientContext context, AppConfiguration appConfig, ConfigItem configItem, String otp, String requestId) {

        var customerAccountCommand = CustomerAccountCommand.builder()
                .rif(context.getFinalCustomerId())
                .bankCode(configItem.getBankCode())
                .currencyCode(configItem.getCurrencyCode())
                .status(configItem.getStatus())
                .statusMe(configItem.getStatusMe())
                .productCv(configItem.getProductCv())
                .product(configItem.getProduct())
                .channel(configItem.getChannel())
                .service(configItem.getService())
                .operator(configItem.getOperator())
                .phone(configItem.getPhone())
                .affiliationStatus(configItem.getAffiliationStatus())
                .counterpartyAccount(configItem.getCounterpartyAccount())
                .holder(configItem.getHolder())
                .build();

        var products = customerProductUseCase.execute(customerAccountCommand, context, appConfig, otp, configItem.isCacheable(), requestId);

        return responseHelper.buildSuccessResponse(
                products.stream().toList(),
                products.isEmpty() ?
                        statusDescriptionResolver.getCode("204") :
                        statusDescriptionResolver.getCode("200"),
                requestId
        );
    }
}