import requests
from korail2 import (
    Korail,
    KorailError,
    NeedToLoginError as KorailLoginError,
    NoResultsError as KorailNoResultsError,
    SoldOutError as KorailSoldOutError,
    TrainType,
    ReserveOption,
    AdultPassenger,
    ChildPassenger,
    ToddlerPassenger,
    SeniorPassenger,
)

from koreantrain.base import Passenger, Reservation, SeatType, Train, TrainService
from koreantrain.errors import LoginError, NetworkError, ReservationError, SoldOutError

_PASSENGER_MAP = {
    "adult": AdultPassenger,
    "child": ChildPassenger,
    "senior": SeniorPassenger,
    "toddler": ToddlerPassenger,
}

_RESERVE_OPTION_MAP = {
    SeatType.GENERAL_FIRST: ReserveOption.GENERAL_FIRST,
    SeatType.GENERAL_ONLY: ReserveOption.GENERAL_ONLY,
    SeatType.SPECIAL_FIRST: ReserveOption.SPECIAL_FIRST,
    SeatType.SPECIAL_ONLY: ReserveOption.SPECIAL_ONLY,
}

TRAIN_TYPES = {
    "all": TrainType.ALL,
    "ktx": TrainType.KTX,
    "saemaeul": TrainType.SAEMAEUL,
    "mugunghwa": TrainType.MUGUNGHWA,
    "itx_saemaeul": TrainType.ITX_SAEMAEUL,
    "itx_cheongchun": TrainType.ITX_CHEONGCHUN,
}


def _convert_passengers(passengers):
    if not passengers:
        return None
    result = []
    for p in passengers:
        cls = _PASSENGER_MAP.get(p.type)
        if cls:
            result.append(cls(p.count))
    return result or None


def _to_train(raw):
    return Train(
        train_name=raw.train_type_name,
        train_number=raw.train_no,
        dep_station=raw.dep_name,
        arr_station=raw.arr_name,
        dep_date=raw.dep_date,
        dep_time=raw.dep_time,
        arr_date=raw.arr_date,
        arr_time=raw.arr_time,
        general_available=raw.has_general_seat(),
        special_available=raw.has_special_seat(),
        _raw=raw,
    )


def _to_reservation(raw):
    return Reservation(
        id=raw.rsv_id,
        total_cost=raw.price,
        seat_count=raw.seat_no_count,
        payment_date=raw.buy_limit_date,
        payment_time=raw.buy_limit_time,
        paid=False,
        train_name=raw.train_type_name,
        train_number=raw.train_no,
        dep_station=raw.dep_name,
        arr_station=raw.arr_name,
        dep_date=raw.dep_date,
        dep_time=raw.dep_time,
        arr_time=raw.arr_time,
        _raw=raw,
    )


class KorailService(TrainService):
    def __init__(self, id, pw, train_type="ktx"):
        self._train_type = TRAIN_TYPES.get(train_type, TrainType.ALL)
        try:
            self._korail = Korail(id, pw)
        except KorailLoginError as e:
            raise LoginError(str(e)) from e
        except requests.exceptions.ConnectionError as e:
            raise NetworkError(repr(e)) from e

    def search(self, dep, arr, date=None, time=None, *, time_limit=None, passengers=None):
        try:
            trains = self._korail.search_train(
                dep, arr, date, time,
                train_type=self._train_type,
                passengers=_convert_passengers(passengers),
            )
            if time_limit:
                trains = [t for t in trains if t.dep_time < time_limit]
            return [_to_train(t) for t in trains]
        except KorailNoResultsError:
            return []
        except KorailLoginError as e:
            raise LoginError(str(e)) from e
        except KorailSoldOutError as e:
            raise SoldOutError(str(e)) from e
        except KorailError as e:
            raise ReservationError(str(e)) from e
        except requests.exceptions.ConnectionError as e:
            raise NetworkError(repr(e)) from e

    def reserve(self, train, *, passengers=None, seat_type=SeatType.GENERAL_FIRST):
        try:
            raw = self._korail.reserve(
                train._raw,
                passengers=_convert_passengers(passengers),
                option=_RESERVE_OPTION_MAP.get(seat_type, ReserveOption.GENERAL_FIRST),
            )
            return _to_reservation(raw)
        except KorailSoldOutError as e:
            raise SoldOutError(str(e)) from e
        except KorailLoginError as e:
            raise LoginError(str(e)) from e
        except KorailError as e:
            raise ReservationError(str(e)) from e
        except requests.exceptions.ConnectionError as e:
            raise NetworkError(repr(e)) from e

    def reservations(self):
        try:
            return [_to_reservation(r) for r in self._korail.reservations()]
        except KorailLoginError as e:
            raise LoginError(str(e)) from e
        except KorailError as e:
            raise ReservationError(str(e)) from e
        except requests.exceptions.ConnectionError as e:
            raise NetworkError(repr(e)) from e

    def cancel(self, reservation):
        try:
            return self._korail.cancel(reservation._raw)
        except KorailLoginError as e:
            raise LoginError(str(e)) from e
        except KorailError as e:
            raise ReservationError(str(e)) from e
        except requests.exceptions.ConnectionError as e:
            raise NetworkError(repr(e)) from e
