"""Phaxor - Pipe Flow Engine (expanded Darcy-Weisbach model)."""

import math

G = 9.81
C_MU = 0.09

PIPE_SCHEDULE_ID_MM = {
    '0.5': {'40': 15.8, '80': 13.8},
    '0.75': {'40': 20.9, '80': 18.9},
    '1': {'40': 26.64, '80': 24.3},
    '1.25': {'40': 35.05, '80': 32.5},
    '1.5': {'40': 40.89, '80': 38.1},
    '2': {'40': 52.5, '80': 49.3},
    '2.5': {'40': 62.7, '80': 59.7},
    '3': {'40': 77.9, '80': 73.7},
    '4': {'40': 102.3, '80': 97.2},
    '5': {'40': 128.2, '80': 122.2},
    '6': {'40': 154.1, '80': 146.3},
    '8': {'40': 202.7, '80': 193.7},
    '10': {'40': 254.5, '80': 242.9},
    '12': {'40': 303.2, '80': 288.9},
}


def _normalize_schedule(schedule: str | None) -> str | None:
    if schedule is None:
        return None
    s = str(schedule).strip().upper()
    if s == 'STD':
        return '40'
    if s == 'XS':
        return '80'
    return s


def _resolve_diameter(inputs: dict) -> tuple[float | None, str | None]:
    d = inputs.get('diameter')
    if isinstance(d, (int, float)) and d > 0:
        return float(d), 'direct'

    nps = inputs.get('nps')
    schedule = _normalize_schedule(inputs.get('schedule'))
    if nps is None or schedule is None:
        return None, None

    by_nps = PIPE_SCHEDULE_ID_MM.get(str(nps).strip())
    if not by_nps:
        return None, None
    id_mm = by_nps.get(schedule)
    if not id_mm:
        return None, None
    return id_mm / 1000.0, f'nps-sch ({nps} / {schedule})'


def _flow_regime(re: float) -> str:
    if re < 2300:
        return 'Laminar'
    if re < 4000:
        return 'Transitional'
    return 'Turbulent'


def _colebrook_white(rel_roughness: float, re: float, max_iter: int = 60) -> float:
    if re <= 0:
        return 0.0
    if re < 2300:
        return 64.0 / re

    f = 0.25 / math.log10(rel_roughness / 3.7 + 5.74 / re**0.9) ** 2
    for _ in range(max_iter):
        sqrt_f = math.sqrt(f)
        rhs = -2.0 * math.log10(rel_roughness / 3.7 + 2.51 / (re * sqrt_f))
        f_new = 1.0 / rhs**2
        if abs(f_new - f) < 1e-9:
            break
        f = f_new
    return f


def _evaluate_at_flow(
    rho: float,
    mu: float,
    diameter: float,
    length: float,
    roughness: float,
    flow_rate: float,
    k_total: float,
    elevation_change: float,
    pump_head: float,
    outlet_pressure: float,
    pump_efficiency: float,
    motor_efficiency: float,
    inlet_pressure: float | None = None,
) -> dict:
    area = math.pi * diameter**2 / 4.0
    velocity = flow_rate / area
    reynolds = (rho * velocity * diameter) / mu
    friction_factor = _colebrook_white(roughness / diameter, reynolds)
    v_head = velocity**2 / (2.0 * G)

    major_head = friction_factor * (length / diameter) * v_head
    minor_head = k_total * v_head
    friction_head = major_head + minor_head
    net_head = friction_head + elevation_change - pump_head
    pressure_drop = rho * G * net_head
    pressure_delta_kpa = pressure_drop / 1000.0
    inlet_p = inlet_pressure if inlet_pressure is not None else outlet_pressure + pressure_delta_kpa

    intensity_pct = max(0.2, 100.0 * 0.16 * max(reynolds, 1.0) ** (-1.0 / 8.0))
    intensity = intensity_pct / 100.0
    lt = 0.07 * diameter
    tke = 1.5 * (intensity * velocity) ** 2
    eps = (C_MU**0.75) * (tke**1.5) / lt if lt > 0 else 0.0
    omega = math.sqrt(tke) / ((C_MU**0.25) * lt) if lt > 0 else 0.0

    hydraulic_power = max(0.0, rho * G * (friction_head + elevation_change) * flow_rate)
    shaft_power = hydraulic_power / max(pump_efficiency, 1e-6)
    motor_power = shaft_power / max(motor_efficiency, 1e-6)

    return {
        'velocity': velocity,
        'reynoldsNumber': reynolds,
        'flowRegime': _flow_regime(reynolds),
        'frictionFactor': friction_factor,
        'pressureDrop': pressure_drop,
        'headLoss': friction_head,
        'area': area,
        'flowRate': flow_rate,
        'massFlowRate': rho * flow_rate,
        'majorHeadLoss': major_head,
        'minorHeadLoss': minor_head,
        'frictionHeadLoss': friction_head,
        'staticHead': elevation_change,
        'pumpHead': pump_head,
        'netHead': net_head,
        'pressureDeltaKPa': pressure_delta_kpa,
        'inletPressure': inlet_p,
        'outletPressure': outlet_pressure,
        'wallShearStress': friction_factor * rho * velocity**2 / 8.0,
        'residenceTime': length / velocity if velocity > 1e-9 else 0.0,
        'equivalentLength': (k_total * diameter) / friction_factor if friction_factor > 0 else 0.0,
        'totalK': k_total,
        'turbulenceIntensityPct': intensity_pct,
        'turbulenceK': tke,
        'turbulenceEpsilon': eps,
        'turbulenceOmega': omega,
        'hydraulicPower': hydraulic_power,
        'shaftPower': shaft_power,
        'motorPower': motor_power,
    }


def _solve_flow_from_head(
    rho: float,
    mu: float,
    diameter: float,
    length: float,
    roughness: float,
    k_total: float,
    available_loss_head: float,
) -> float | None:
    if available_loss_head <= 0:
        return 0.0

    area = math.pi * diameter**2 / 4.0

    def loss_at_velocity(velocity: float) -> float:
        re = (rho * velocity * diameter) / mu
        f = _colebrook_white(roughness / diameter, re)
        return (f * (length / diameter) + k_total) * velocity**2 / (2.0 * G)

    low = 0.0
    high = 0.05
    while loss_at_velocity(high) < available_loss_head and high < 120:
        high *= 2.0
    if high >= 120 and loss_at_velocity(high) < available_loss_head:
        return None

    for _ in range(90):
        mid = 0.5 * (low + high)
        if loss_at_velocity(mid) > available_loss_head:
            high = mid
        else:
            low = mid

    return area * 0.5 * (low + high)


def solve_pipe_flow(inputs: dict) -> dict | None:
    """Detailed Darcy-Weisbach pipe flow solver with dual boundary modes and NPS/SCH support."""
    solve_mode = inputs.get('solveMode', 'known-flow')
    flow_input_mode = inputs.get('flowInputMode', 'volumetric')

    diameter, diameter_source = _resolve_diameter(inputs)
    if diameter is None:
        return None

    length = float(inputs.get('length', 0))
    roughness = float(inputs.get('roughness', 0.000045))
    rho = float(inputs.get('density', 998))
    mu = float(inputs.get('viscosity', 0.001002))
    k_total = float(inputs.get('minorLossK', 0))
    elevation_change = float(inputs.get('elevationChange', 0))
    pump_head = float(inputs.get('pumpHead', 0))
    inlet_pressure = float(inputs.get('inletPressure', 0))
    outlet_pressure = float(inputs.get('outletPressure', 0))
    pump_eff = max(float(inputs.get('pumpEfficiency', 72)) / 100.0, 1e-6)
    motor_eff = max(float(inputs.get('motorEfficiency', 92)) / 100.0, 1e-6)

    if diameter <= 0 or length <= 0 or roughness < 0 or rho <= 0 or mu <= 0:
        return None

    if solve_mode == 'known-flow':
        area = math.pi * diameter**2 / 4.0
        flow_rate = float(inputs.get('flowRate', 0))
        if 'flowValue' in inputs:
            flow_value = float(inputs.get('flowValue', 0))
            if flow_input_mode == 'volumetric':
                flow_rate = flow_value / 3600.0
            elif flow_input_mode == 'mass':
                flow_rate = flow_value / rho
            else:
                flow_rate = flow_value * area
        if flow_rate <= 0:
            return None

        result = _evaluate_at_flow(
            rho=rho,
            mu=mu,
            diameter=diameter,
            length=length,
            roughness=roughness,
            flow_rate=flow_rate,
            k_total=k_total,
            elevation_change=elevation_change,
            pump_head=pump_head,
            outlet_pressure=outlet_pressure,
            pump_efficiency=pump_eff,
            motor_efficiency=motor_eff,
            inlet_pressure=inputs.get('inletPressure'),
        )
    else:
        available_loss_head = ((inlet_pressure - outlet_pressure) * 1000.0) / (rho * G) + pump_head - elevation_change
        if available_loss_head <= 0:
            return None

        flow_rate = _solve_flow_from_head(
            rho=rho,
            mu=mu,
            diameter=diameter,
            length=length,
            roughness=roughness,
            k_total=k_total,
            available_loss_head=available_loss_head,
        )
        if flow_rate is None:
            return None

        result = _evaluate_at_flow(
            rho=rho,
            mu=mu,
            diameter=diameter,
            length=length,
            roughness=roughness,
            flow_rate=flow_rate,
            k_total=k_total,
            elevation_change=elevation_change,
            pump_head=pump_head,
            outlet_pressure=outlet_pressure,
            pump_efficiency=pump_eff,
            motor_efficiency=motor_eff,
            inlet_pressure=inlet_pressure,
        )

    result.update({
        'solveMode': solve_mode,
        'flowInputMode': flow_input_mode,
        'diameterUsed': diameter,
        'diameterSource': diameter_source,
        'nps': inputs.get('nps'),
        'schedule': inputs.get('schedule'),
    })
    return result
