import logging
import sys

import grpc

from ..grpcclient import uncertainty_grpc_client, uncertaintyMessages_pb2
from ..helpers.exception import AuthenticationError, P90ConnectionError, ServiceError
from ..helpers.request_helpers import format_number_for_display


class P90Client:
    """A gRPC client for the PV Lighthouse P90 analysis service."""

    def __init__(self, api_base_uri: str = "middleware.pvlighthouse.com.au", version: str = "latest-prod"):
        """Initialize the P90Client.
        Args:
            api_base_uri (str): The base URI for the API. Points to the production server by default
            version (str): The version of the API to use. Points to the latest production version by default

        Note:
            The API base URI and version can be overridden for testing or development purposes. This can also
            pointing specifically to a previous version of the service (eg: 7.20.0) should the client become
            incompatible with the latest version and while a later version of the client is not available.
        """
        try:
            self.channel = grpc.secure_channel(api_base_uri, grpc.ssl_channel_credentials())
            self.client = uncertainty_grpc_client.UncertaintyStub(self.channel)
            self.metadata = [("version", version)]
            if self.is_dev_version(version):
                self.metadata.append(("x-cloudfront-env", "dev"))
            else:
                self.metadata.append(("x-cloudfront-env", "prod"))
            logging.info("Connected to P90 service at %s", api_base_uri)
        except Exception as e:
            raise P90ConnectionError(f"Failed to connect to P90 service at {api_base_uri}: {e}") from e

    def is_dev_version(self, version: str) -> bool:
        """Determine if the provided version string indicates a development version."""
        if "latest-dev" == version:
            return True

        version_split = version.split(".")
        if len(version_split) >= 4:
            return True

        return False

    def _handle_grpc_error(self, error: grpc.RpcError, timeout: float | None) -> None:
        """Handle specific gRPC errors and raise appropriate exceptions."""
        error_handlers = {
            grpc.StatusCode.UNAUTHENTICATED: lambda: AuthenticationError("Authentication failed. Please check your credentials."),
            grpc.StatusCode.PERMISSION_DENIED: lambda: AuthenticationError(
                "Permission denied. Please check your account permissions and contact support@pvlighthouse.com.au."
            ),
            grpc.StatusCode.RESOURCE_EXHAUSTED: lambda: ServiceError(f"{error.details()}. Please contact support@pvlighthouse.com.au."),
            grpc.StatusCode.UNIMPLEMENTED: lambda: ServiceError("Service endpoint not implemented or not found."),
            grpc.StatusCode.UNAVAILABLE: lambda: P90ConnectionError("Service is currently unavailable. Please try again later."),
            grpc.StatusCode.DEADLINE_EXCEEDED: lambda: TimeoutError(f"Request timed out after {timeout} seconds."),
            grpc.StatusCode.INVALID_ARGUMENT: lambda: ValueError(f"Invalid request parameters: {error.details()}"),
            grpc.StatusCode.CANCELLED: lambda: P90ConnectionError("Request was cancelled. This may happen if the stream is closed unexpectedly."),
            grpc.StatusCode.ABORTED: lambda: P90ConnectionError("Request was aborted by the server. The stream may have been interrupted."),
        }

        if handler := error_handlers.get(error.code()):
            raise handler() from error
        raise ServiceError(f"Service error ({error.code().name}): {error.details()}") from error

    def _log_inputs_used(self, used_inputs: uncertaintyMessages_pb2.UncertaintyUsedInputs) -> None:
        """Log comprehensive details about the inputs used in the P90 analysis."""
        logging.debug("=== P90 Analysis Inputs Used ===")

        # Module Information
        if used_inputs.HasField("Module"):
            module = used_inputs.Module
            logging.debug(
                "Module: Length=%.3fm, Width=%.3fm, PowerRatingAtSTC=%.1fW, CellToCellMismatch=%.4f, PowerTempCoeff=%.6f, Bifaciality=%.3f",
                module.LengthInM,
                module.WidthInM,
                module.PowerRatingAtSTCInW,
                module.CellToCellMismatch,
                module.ModulePowerTemperatureCoefficient,
                module.Bifaciality,
            )
        else:
            logging.debug("Module: No module information provided")

        # System Information
        if used_inputs.HasField("System"):
            system = used_inputs.System
            logging.debug(
                "System: ModulesPerString=%d, StringsPerInverter=%d, NumberOfInverters=%d",
                system.ModulesPerString,
                system.StringsPerInverter,
                system.NumberOfInverters,
            )
            logging.debug(
                "System Geometry: RowPitch=%.2fm, ModuleAzimuth=%.1f°, ModuleHeight=%.3fm, FallbackModuleTilt=%.1f°",
                system.RowPitchInM,
                system.ModuleAzimuthInDegrees,
                system.ModuleHeight,
                system.FallbackModuleTiltInDegrees,
            )
            logging.debug(
                "System Tracking: ModuleTracking=%s, TiltLimit=%.1f°",
                system.ModuleTracking,
                system.TiltLimitInDegrees,
            )
        else:
            logging.debug("System: No system information provided")

        # Electrical System Information
        if used_inputs.HasField("Electrical"):
            electrical = used_inputs.Electrical
            logging.debug(
                "Electrical Efficiencies: InverterEff=%.4f, ModuleToModuleMismatch=%.4f, StringWiringLoss=%.4f, InverterClipping=%s",
                electrical.InverterEfficiency,
                electrical.ModuleToModuleMismatch,
                electrical.StringWiringLoss,
                format_number_for_display(electrical.InverterClipping),
            )
            logging.debug(
                "Electrical Losses: MaxPowerTracking=%.4f, InverterWiring=%.4f, StringToStringMismatch=%.4f, InverterToInverterMismatch=%.4f",
                electrical.MaxPowerTrackingLoss,
                electrical.InverterWiringLoss,
                electrical.StringToStringMismatch,
                electrical.InverterToInverterMismatch,
            )
        else:
            logging.debug("Electrical settings: No electrical information provided")

        # Optical System Information
        if used_inputs.HasField("Optical"):
            optical = used_inputs.Optical
            logging.debug(
                "Optical Multipliers: BeamFront=%.3f, BeamRear=%.3f, IsotropicFront=%.3f, IsotropicRear=%.3f",
                optical.BeamMultiplierFront,
                optical.BeamMultiplierRear,
                optical.IsotropicMultiplierFront,
                optical.IsotropicMultiplierRear,
            )
            logging.debug(
                "Optical Fallbacks: Albedo=%.3f, SoilingFront=%.4f, SoilingRear=%.4f, SpectralCorrection=%.4f, ExtraIrradiance=%.4f, "
                "RearTransmissionFactor=%.3f, RearStructuralShadingFactor=%.3f",
                optical.FallbackAlbedo,
                optical.FallbackSoilingFront,
                optical.FallbackSoilingRear,
                optical.FallbackSpectralCorrection,
                optical.ExtraIrradiance,
                optical.FallbackRearTransmissionFactor,
                optical.FallbackRearStructuralShadingFactor,
            )
        else:
            logging.debug("Optical settings: No optical information provided")

        # Thermal system Information
        if used_inputs.HasField("Thermal"):
            thermal = used_inputs.Thermal
            logging.debug(
                "Thermal: Uc=%.2f, Uv=%.2f, Alpha=%.6f",
                thermal.Uc,
                thermal.Uv,
                thermal.Alpha,
            )
        else:
            logging.debug("Thermal settings: No thermal information provided")

        # Operational System Information
        if used_inputs.HasField("Operation"):
            operational = used_inputs.Operation
            logging.debug(
                "Operational: AnnualDegradationRate=%.4f, Curtailment=%.4f, Availability=%.4f, DCHealth=%.4f, UndulatingGround=%f, YieldModifier=%.4f",
                operational.AnnualDegradationRate,
                operational.Curtailment,
                operational.Availability,
                operational.DCHealth,
                operational.UndulatingGround,
                operational.YieldModifier,
            )
        else:
            logging.debug("Operational settings: No operational information provided")

        # Simulation Options
        if used_inputs.HasField("SimulationOptions"):
            sim_opts = used_inputs.SimulationOptions
            logging.debug(
                "Simulation: %d years, %d simulations",
                sim_opts.NumberOfYears,
                sim_opts.NumberOfSimulations,
            )
        else:
            logging.debug("Simulation: No simulation options provided")

        # Result Options Information
        if used_inputs.HasField("ResultOptions"):
            result_options = used_inputs.ResultOptions
            p_values_str = ",".join(map(str, result_options.PValues)) if result_options.PValues else "None"
            logging.debug(
                "Result Options: BinMin=%.2f, BinDelta=%.2f, PValues=[%s]",
                result_options.BinMin,
                result_options.BinDelta,
                p_values_str,
            )
        else:
            logging.debug("Result options: No result options provided")

        logging.debug("=== End Inputs Used ===")

    def send_request(
        self,
        request: uncertaintyMessages_pb2.UncertaintyRequest,
        call_credentials: grpc.CallCredentials,
        timeout: float | None = 120.0,
    ) -> tuple[uncertaintyMessages_pb2.UncertaintySummary | None, uncertaintyMessages_pb2.UncertaintyUsedInputs | None]:
        """Send a P90 analysis request and return the summary results and used inputs."""
        if not request.ActualYearlyDataSet.TimeStepDataPoints and not request.TMYDataSet.TimeStepDataPoints:
            raise ValueError("Request must contain actual yearly data or TMY data")

        summary = None
        used_inputs = None

        try:
            logging.info("Starting P90 analysis request")
            response_stream = self.client.UncertaintyAnalysis(request, metadata=self.metadata, credentials=call_credentials, timeout=timeout)

            for response in response_stream:
                received: uncertaintyMessages_pb2.UncertaintyResponse = response

                if received.HasField("InputsUsed"):
                    used_inputs = received.InputsUsed
                    self._log_inputs_used(used_inputs)

                # Overwrite the same line with progress
                sys.stdout.write(f"\rProgress: {received.pctComplete * 100:.2f}%")
                sys.stdout.flush()

                # Check for summary in response
                if received.HasField("Summary"):
                    summary = received.Summary
                    print("\n")
                    logging.info("Received P90 analysis summary")
                    print(f"Analysis complete! Summary contains {len(summary.YearlyPValue)} yearly P-values")

        except grpc.RpcError as e:
            print("\n")
            logging.error("gRPC error during P90 analysis: %s", e)
            self._handle_grpc_error(e, timeout)
        except (ValueError, TypeError, AttributeError) as e:
            print("\n")
            logging.error("Unexpected error during P90 analysis: %s", e)
            raise P90ConnectionError(f"Unexpected error during P90 analysis: {e}") from e

        if not summary or not summary.YearlyPValue:
            print("\n")
            logging.warning("No results received from P90 analysis")

        return summary, used_inputs

    def close(self) -> None:
        """Close the gRPC channel and clean up resources."""
        try:
            self.channel.close()
            logging.info("P90 client connection closed")
        except Exception as e:
            logging.warning("Error closing P90 client: %s", e)
            raise P90ConnectionError(f"Unexpected error during P90 analysis: {e}") from e

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.close()
