#!/usr/bin/env python3
"""Calculate relative humidity from dry-bulb and wet-bulb temperatures.

Values may be supplied either as named flags (``-tdb``/``-twb``/``-p``) or as
three bare positional values in any order. Positional values are classified
automatically by range: the value within the pressure band is taken as the
atmospheric pressure, the larger of the two remaining values as the dry-bulb
temperature and the smaller as the wet-bulb temperature.
"""

from __future__ import annotations

import argparse
import math
import sys

PRESSURE_MIN = 900
PRESSURE_MAX = 1100
TEMPERATURE_MIN = -40.0
TEMPERATURE_MAX = 50.0


def main() -> None:
    """Parse arguments, compute relative humidity and print the result."""
    args = setup_argparse().parse_args()

    try:
        dry_bulb, wet_bulb, pressure = resolve_inputs(args)
    except ValueError as error:
        sys.exit(f"Error: {error}")

    relative_humidity = calculate(
        temperature_dry_bulb=dry_bulb,
        temperature_wet_bulb=wet_bulb,
        pressure=pressure,
    )

    print(f"Relative Humidity: {relative_humidity:.1f}%")


def setup_argparse() -> argparse.ArgumentParser:
    """Build the command-line argument parser."""
    parser = argparse.ArgumentParser(
        description=(
            "Calculate relative humidity from dry-bulb and wet-bulb "
            "temperatures."
        ),
        epilog=(
            "Provide three bare values in any order, e.g. "
            "`psychrometric 25 18 1013`. The value in the "
            f"{PRESSURE_MIN}-{PRESSURE_MAX} hPa band is treated as pressure, "
            "the larger remaining value as dry-bulb temperature and the "
            "smaller as wet-bulb temperature. Named flags may be used instead."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "values",
        nargs="*",
        type=float,
        metavar="VALUE",
        help=(
            "Three bare values in any order: two temperatures in "
            f"{TEMPERATURE_MIN:g} to {TEMPERATURE_MAX:g} C and one pressure "
            f"in {PRESSURE_MIN}-{PRESSURE_MAX} hPa."
        ),
    )
    parser.add_argument(
        "-tdb",
        "--temperature-dry-bulb",
        type=float,
        help="Dry-bulb temperature in degrees Celsius.",
    )
    parser.add_argument(
        "-twb",
        "--temperature-wet-bulb",
        type=float,
        help="Wet-bulb temperature in degrees Celsius.",
    )
    parser.add_argument(
        "-p",
        "--pressure",
        type=int,
        help="Atmospheric pressure in hPa.",
    )

    return parser


def resolve_inputs(args: argparse.Namespace) -> tuple[float, float, int]:
    """Resolve dry-bulb, wet-bulb and pressure from parsed arguments.

    Either three positional values or the full set of named flags must be
    supplied, but not a mixture of the two.
    """
    named = (
        args.temperature_dry_bulb,
        args.temperature_wet_bulb,
        args.pressure,
    )
    has_named = any(value is not None for value in named)
    has_positional = bool(args.values)

    if has_named and has_positional:
        raise ValueError(
            "Mixing positional values with named flags is not supported; "
            "use one style or the other."
        )

    if has_positional:
        return classify_values(args.values)

    if all(value is not None for value in named):
        dry_bulb = args.temperature_dry_bulb
        wet_bulb = args.temperature_wet_bulb
        pressure = args.pressure
        validate_ranges(dry_bulb, wet_bulb, pressure)
        return dry_bulb, wet_bulb, pressure

    raise ValueError(
        "Provide three values, either positionally (e.g. `25 18 1013`) "
        "or via -tdb/-twb/-p."
    )


def classify_values(values: list[float]) -> tuple[float, float, int]:
    """Classify three bare values into dry-bulb, wet-bulb and pressure.

    The single value within the pressure band is taken as pressure; of the
    remaining two, the larger is the dry-bulb temperature and the smaller the
    wet-bulb temperature.
    """
    if len(values) != 3:
        raise ValueError(f"Expected exactly three values, got {len(values)}.")

    for value in values:
        in_pressure = PRESSURE_MIN <= value <= PRESSURE_MAX
        in_temperature = TEMPERATURE_MIN <= value <= TEMPERATURE_MAX
        if not (in_pressure or in_temperature):
            raise ValueError(
                f"Value {value:g} is out of range: expected a temperature "
                f"({TEMPERATURE_MIN:g} to {TEMPERATURE_MAX:g} C) or a "
                f"pressure ({PRESSURE_MIN}-{PRESSURE_MAX} hPa)."
            )

    pressures = [
        v for v in values if PRESSURE_MIN <= v <= PRESSURE_MAX
    ]
    temperatures = [
        v for v in values if TEMPERATURE_MIN <= v <= TEMPERATURE_MAX
    ]

    if len(pressures) != 1:
        raise ValueError(
            "Ambiguous input: expected exactly one pressure value in "
            f"{PRESSURE_MIN}-{PRESSURE_MAX} hPa, found {len(pressures)}."
        )

    pressure = int(round(pressures[0]))
    dry_bulb = max(temperatures)
    wet_bulb = min(temperatures)

    return dry_bulb, wet_bulb, pressure


def validate_ranges(
    temperature_dry_bulb: float,
    temperature_wet_bulb: float,
    pressure: int,
) -> None:
    """Validate that supplied values fall within the supported ranges."""
    for label, value in (
        ("Dry-bulb temperature", temperature_dry_bulb),
        ("Wet-bulb temperature", temperature_wet_bulb),
    ):
        if not TEMPERATURE_MIN <= value <= TEMPERATURE_MAX:
            raise ValueError(
                f"{label} {value:g} C is out of range "
                f"({TEMPERATURE_MIN:g} to {TEMPERATURE_MAX:g} C)."
            )

    if not PRESSURE_MIN <= pressure <= PRESSURE_MAX:
        raise ValueError(
            f"Pressure {pressure} hPa is out of range "
            f"({PRESSURE_MIN}-{PRESSURE_MAX} hPa)."
        )

    if temperature_wet_bulb > temperature_dry_bulb:
        raise ValueError(
            "Wet-bulb temperature cannot exceed dry-bulb temperature."
        )


def calculate(
    temperature_dry_bulb: float,
    temperature_wet_bulb: float,
    pressure: float,
) -> float:
    """Calculate relative humidity from temperatures and pressure.

    Parameters
    ----------
    temperature_dry_bulb:
        Dry-bulb temperature in degrees Celsius.
    temperature_wet_bulb:
        Wet-bulb temperature in degrees Celsius.
    pressure:
        Atmospheric pressure in hPa.

    Returns
    -------
    float
        Relative humidity as a percentage.
    """
    a = 17.2694
    b = 237.3

    alpha_dry = (a * temperature_dry_bulb) / (b + temperature_dry_bulb)
    saturation_vapor_pressure_dry = 0.6108 * math.exp(alpha_dry)

    alpha_wet = (a * temperature_wet_bulb) / (b + temperature_wet_bulb)
    saturation_vapor_pressure_wet = 0.6108 * math.exp(alpha_wet)

    actual_vapor_pressure = saturation_vapor_pressure_wet - (
        pressure
        * 0.1
        * (temperature_dry_bulb - temperature_wet_bulb)
        * 0.00066
        * (1 + 0.00115 * temperature_wet_bulb)
    )

    relative_humidity = (
        actual_vapor_pressure / saturation_vapor_pressure_dry
    ) * 100

    return relative_humidity


if __name__ == "__main__":
    main()
