from datetime import datetime

from sqlalchemy import (
    TIMESTAMP,
    BigInteger,
    Boolean,
    Column,
    Float,
    ForeignKey,
    Integer,
    SmallInteger,
    String,
    UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import ARRAY as PSQL_ARRAY
from sqlalchemy.dialects.postgresql import INTEGER as PSQL_INTEGER
from sqlalchemy.dialects.postgresql import JSONB as PSQL_JSONB
from sqlalchemy.dialects.postgresql import TEXT as PSQL_TEXT
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import relationship

Base = declarative_base()


class Participant(Base):
    """
    A participant class, describing the participants table.

    Attributes
    ----------
    id: int
        The primary key for this table.
    username: str(64)
        The primary pseudoanonymous identifier for each participants.
        For true participants, these were assigned by E4 Connect as usernames for Empatica Studies
        and had the form of uploader_0123456.
    password_hash: str(128)
    token: str(32)
        Authentication is implemented using HTTP Basic Auth and tokens are provided for this purpose.
    token_expiration_utc: datetime
        Tokens expire after a preset time period.
    collection_start_utc: datetime
        A datetime of the first data point uploaded.
    last_upload_utc: datetime
        A datetime of the last data point uploaded.
    day_count_uploaded: str
        Days between last_upload_utc and collection_start_utc.
    last_known_device_id: str(36)
        Device (installation) ID that was last seen in the data.
    active: bool
        Is participant marked as active (i.e. collecting data)?
    marked_active_utc: datetime
        The time participant was first marked as active.
    day_count_active: int
        Days from marked_active_utc
    tester: bool
        Is this a tester (or a true participant)?
    """

    __tablename__ = "participants"
    id = Column(Integer, primary_key=True)
    username = Column(String(64), index=True, unique=True)
    password_hash = Column(String(128))
    token = Column(String(32), index=True, unique=True)
    token_expiration_utc = Column(TIMESTAMP(timezone=False))

    collection_start_utc = Column(TIMESTAMP(timezone=False))
    last_upload_utc = Column(TIMESTAMP(timezone=False))
    day_count_uploaded = Column(SmallInteger)
    last_known_device_id = Column(String(length=36), nullable=False, default="")

    active = Column(Boolean, index=True, nullable=False, default=False)
    marked_active_utc = Column(TIMESTAMP(timezone=False))
    day_count_active = Column(SmallInteger)

    tester = Column(Boolean, index=True, nullable=False, default=False)

    def __repr__(self):
        return "Participant()"

    def __str__(self):
        return "<Participant {}>".format(self.username)


class AWAREsensor(object):
    """
    A general AWARE sensor class. It includes fields common to all tables/subclasses.

    Attributes
    ----------
    id: int
        The primary key for each table.
    _id: int
        The original primary key, used in device's own MySQL database.
    timestamp: int
        Unixtime milliseconds since 1970.
    device_id: str(36)
        AWARE device UUID, a unique string of length 36.
        Rather than identifying the device as the name suggests, this string changes on each (re)installation.
        It is therefore better considered to be an installation ID.

    Declared Attributes
    -------------------
    __tablename__: str
        Table name used in the database.
    participant_id: int
        The foreign key relating (with the relationship) tables to the participants table.
    """

    id = Column(BigInteger, primary_key=True, nullable=False)
    _id = Column(BigInteger, nullable=False)
    timestamp = Column(BigInteger, nullable=False)
    device_id = Column(String(length=36), nullable=False)

    @declared_attr
    def __tablename__(self):
        return self.__name__.lower()

    @declared_attr
    def participant_id(self):
        return Column(
            Integer, ForeignKey("participants.id"), nullable=False, index=True
        )

    @declared_attr
    def participant(self):
        return relationship("Participant", lazy="select", backref=self.__tablename__)

    @declared_attr
    def __table_args__(self):
        return (
            UniqueConstraint("device_id", "_id", name=self.__tablename__ + "_twice"),
        )

    # I think it makes more sense to create a Constraint
    # on device_id and _id rather than relate it to participant_id.
    # _id is a primary key, auto incremented by AWARE.
    # However, I would expect it to reset back to 1
    # if the application was reinstalled,
    # similarly to how device_id resets.


class Accelerometer(Base, AWAREsensor):
    double_values_0 = Column(Float, nullable=False)
    double_values_1 = Column(Float, nullable=False)
    double_values_2 = Column(Float, nullable=False)
    accuracy = Column(SmallInteger, nullable=True)
    label = Column(String, nullable=True)


class GoogleAR(Base, AWAREsensor):
    __tablename__ = "google_ar"
    activity_name = Column(String(32), nullable=True)
    activity_type = Column(SmallInteger, nullable=True)
    confidence = Column(SmallInteger, nullable=True)
    activities = Column(PSQL_JSONB(none_as_null=False), nullable=True)


class Application(Base, AWAREsensor):
    __tablename__ = "applications"
    #    package_name = Column(String, nullable=False)
    #    application_name = Column(String)
    package_hash = Column(String(64), nullable=False)
    play_store_genre = Column(String, nullable=True)
    is_system_app = Column(Boolean)


class Barometer(Base, AWAREsensor):
    """
    Contains the barometer sensor data.

    Attributes
    ----------
    double_values_0: float
        The ambient air pressure in mbar (hPa)
    accuracy: int
        Sensor’s accuracy level, either 1, 2, or 3 (see [SensorManager](https://developer.android.com/reference/android/hardware/SensorManager.html#SENSOR_STATUS_ACCURACY_HIGH))
    """

    double_values_0 = Column(Float, nullable=False)
    accuracy = Column(SmallInteger, nullable=True)
    label = Column(String, nullable=True)


class BarometerSensor(Base, AWAREsensor):
    """
    Contains the barometer sensor capabilities.

    Attributes
    ----------
    double_sensor_maximum_range: float
        Maximum sensor value possible
    double_sensor_minimum_delay: float
        Minimum sampling delay in microseconds
    sensor_name: str
    double_sensor_power_ma: float
        Sensor’s power drain in mA
    double_sensor_resolution: float
        Sensor’s resolution in sensor’s units
    sensor_type: str
    sensor_vendor: str
        Sensor’s manufacturer
    sensor_version: str
    """

    __tablename__ = "barometer_sensor"
    # Since this table is not really important,
    # I will leave all columns as nullable. (nullable=True by default.)
    double_sensor_maximum_range = Column(Float)
    double_sensor_minimum_delay = Column(Float)
    sensor_name = Column(String)
    double_sensor_power_ma = Column(Float)
    double_sensor_resolution = Column(Float)
    sensor_type = Column(String)
    sensor_vendor = Column(String)
    sensor_version = Column(String)


class Battery(Base, AWAREsensor):
    battery_status = Column(SmallInteger, nullable=False)
    battery_level = Column(SmallInteger, nullable=False)
    battery_scale = Column(SmallInteger, nullable=False)
    battery_voltage = Column(Integer, nullable=True)
    battery_temperature = Column(Integer, nullable=True)
    battery_adaptor = Column(SmallInteger, nullable=True)
    battery_health = Column(SmallInteger, nullable=True)
    battery_technology = Column(String, nullable=True)


class Bluetooth(Base, AWAREsensor):
    bt_address = Column(String(length=40), nullable=False)
    bt_name = Column(String, nullable=True)
    bt_rssi = Column(Integer, nullable=True)
    label = Column(String, nullable=True)


class Call(Base, AWAREsensor):
    """
    Contains the calls sensor information.

    Attributes
    ----------
    call_type: int
        One of the Android’s call types (1 – incoming, 2 – outgoing, 3 – missed).
    call_duration: int
        Length of the call session in seconds.
    trace: str(40)
        A hash value SHA-1 of the phone number (source or target) of the call
    """

    call_type = Column(SmallInteger, nullable=False)
    call_duration = Column(Integer, nullable=False)
    trace = Column(String(length=40), nullable=True)


class ESM(Base, AWAREsensor):
    esm_status = Column(SmallInteger, nullable=False)
    esm_user_answer = Column(String, nullable=True)
    esm_notification_timeout = Column(SmallInteger)
    esm_json = Column(PSQL_JSONB(none_as_null=False), nullable=False)
    double_esm_user_answer_timestamp = Column(BigInteger, nullable=False)
    esm_trigger = Column(String, nullable=True)
    esm_session = Column(Integer, nullable=False)
    esm_notification_id = Column(Integer, nullable=False)
    esm_expiration_threshold = Column(SmallInteger)
    ESM_TYPE = {
        "text": 1,
        "radio": 2,
        "checkbox": 3,
        "likert": 4,
        "quick_answers": 5,
        "scale": 6,
        "datetime": 7,
        "pam": 8,
        "number": 9,
        "web": 10,
        "date": 11,
    }


class Imperfection(Base):
    __tablename__ = "imperfection"
    id = Column(BigInteger, primary_key=True, nullable=False)
    timestamp = Column(BigInteger, nullable=False)
    error = Column(String)
    data_type = Column(String)
    data = Column(String)


class LightSensor(Base, AWAREsensor):
    """
    Contains the light sensor data.
    Note: Even though this table is named light_sensor, it actually contains what AWARE calls light data
        (rather than the data about the sensor's capabilities). Cf. Barometer(Sensor) and Temperature(Sensor).

    Attributes
    ----------
    double_light_lux: float
        The ambient luminance in lux units
    accuracy: int
        Sensor’s accuracy level, either 1, 2, or 3 (see [SensorManager](https://developer.android.com/reference/android/hardware/SensorManager.html#SENSOR_STATUS_ACCURACY_HIGH))
    """

    __tablename__ = "light_sensor"
    double_light_lux = Column(Float, nullable=False)
    accuracy = Column(Integer, nullable=True)
    label = Column(String, nullable=True)


class Location(Base, AWAREsensor):
    __tablename__ = "locations"
    double_latitude = Column(Float, nullable=False)
    double_longitude = Column(Float, nullable=False)
    double_bearing = Column(Float)
    double_speed = Column(Float)
    double_altitude = Column(Float)
    provider = Column(String)
    accuracy = Column(Integer)
    label = Column(String, nullable=True)
    category = Column(PSQL_ARRAY(PSQL_TEXT), nullable=True)
    category_short = Column(PSQL_ARRAY(PSQL_TEXT), nullable=True)
    distance = Column(PSQL_ARRAY(PSQL_INTEGER), nullable=True)
    api_response_code = Column(SmallInteger, nullable=False)
    api_response = Column(String, nullable=True)


class Mic(Base, AWAREsensor):
    half_s_speech = Column(Boolean, nullable=False)
    rnnoutp = Column(Float, nullable=False)
    flatness = Column(Float, nullable=False)
    absmax = Column(Float, nullable=False)
    max = Column(Float, nullable=False)
    min = Column(Float, nullable=False)
    # mfcc = Column(PSQL_ARRAY(PSQL_REAL, dimensions=13), nullable=False)
    mfcc_0 = Column(Float, nullable=False)
    mfcc_1 = Column(Float, nullable=False)
    mfcc_2 = Column(Float, nullable=False)
    mfcc_3 = Column(Float, nullable=False)
    mfcc_4 = Column(Float, nullable=False)
    mfcc_5 = Column(Float, nullable=False)
    mfcc_6 = Column(Float, nullable=False)
    mfcc_7 = Column(Float, nullable=False)
    mfcc_8 = Column(Float, nullable=False)
    mfcc_9 = Column(Float, nullable=False)
    mfcc_10 = Column(Float, nullable=False)
    mfcc_11 = Column(Float, nullable=False)
    mfcc_12 = Column(Float, nullable=False)


class Speech(Base, AWAREsensor):
    speech_proportion = Column(Float, nullable=False)


class NetworkData(Base, AWAREsensor):
    __tablename__ = "network_data"
    network_type = Column(SmallInteger, nullable=False)
    network_subtype = Column(String(10), nullable=False)
    network_state = Column(SmallInteger, nullable=True)


class NetworkTraffic(Base, AWAREsensor):
    __tablename__ = "network_traffic_data"
    network_type = Column(SmallInteger, nullable=False)
    double_received_bytes = Column(Float, nullable=True)
    double_sent_bytes = Column(Float, nullable=True)
    double_received_packets = Column(Float, nullable=True)
    double_sent_packets = Column(Float, nullable=True)


class Notification(Base, AWAREsensor):
    __tablename__ = "notifications"
    #    package_name = Column(String, nullable=False)
    #    application_name = Column(String, nullable=False)
    package_hash = Column(String(64), nullable=False)
    play_store_genre = Column(String, nullable=True)
    sound = Column(String, nullable=False)
    vibrate = Column(String, nullable=False)
    sound_app = Column(String, nullable=False)
    vibrate_app = Column(String, nullable=False)
    led_app = Column(String, nullable=False)
    category = Column(String, nullable=False)
    is_ongoing = Column(SmallInteger, nullable=False)
    is_clearable = Column(SmallInteger, nullable=False)
    is_group = Column(SmallInteger, nullable=True)


class Processor(Base, AWAREsensor):
    double_last_user = Column(Float, nullable=False)
    double_last_system = Column(Float, nullable=False)
    double_last_idle = Column(Float, nullable=False)
    double_user_load = Column(Float, nullable=False)
    double_system_load = Column(Float, nullable=False)
    double_idle_load = Column(Float, nullable=False)


class Proximity(Base, AWAREsensor):
    double_proximity = Column(Float, nullable=False)
    accuracy = Column(SmallInteger, nullable=True)
    label = Column(String, nullable=True)


class Screen(Base, AWAREsensor):
    screen_status = Column(SmallInteger)


class SMS(Base, AWAREsensor):
    """
    Contains the messages sensor information.

    Attributes
    ----------
    message_type: int
        Message type (1 – received, 2 – sent)
    trace: str(40)
        A hash value SHA-1 of the phone number (source or target) of the call
    """

    message_type = Column(SmallInteger, nullable=False)
    trace = Column(String(length=40), nullable=False)


class Temperature(Base, AWAREsensor):
    """
    Contains the temperature sensor data.

    Attributes
    ----------
    temperature_celsius: float
        Measured temperature in °C
    accuracy: int
        Sensor’s accuracy level, either 1, 2, or 3 (see [SensorManager](https://developer.android.com/reference/android/hardware/SensorManager.html#SENSOR_STATUS_ACCURACY_HIGH))
    """

    temperature_celsius = Column(Float, nullable=False)
    accuracy = Column(SmallInteger, nullable=True)
    label = Column(String, nullable=True)


class TemperatureSensor(Base, AWAREsensor):
    """
    Contains the temperature sensor capabilities.

    Attributes
    ----------
    double_sensor_maximum_range: float
        Maximum sensor value possible
    double_sensor_minimum_delay: float
        Minimum sampling delay in microseconds
    sensor_name: str
    double_sensor_power_ma: float
        Sensor’s power drain in mA
    double_sensor_resolution: float
        Sensor’s resolution in sensor’s units
    sensor_type: str
    sensor_vendor: str
        Sensor’s manufacturer
    sensor_version: str
    """

    # I left all of these nullable,
    # as we haven't seen any data from this sensor anyway.
    __tablename__ = "temperature_sensor"
    double_sensor_maximum_range = Column(Float)
    double_sensor_minimum_delay = Column(Float)
    sensor_name = Column(String)
    double_sensor_power_ma = Column(Float)
    double_sensor_resolution = Column(Float)
    sensor_type = Column(String)
    sensor_vendor = Column(String)
    sensor_version = Column(String)


class Timezone(Base, AWAREsensor):
    timezone = Column(String, nullable=False)


class WiFi(Base, AWAREsensor):
    bssid = Column(String(length=40), nullable=False)
    ssid = Column(String, nullable=True)
    rssi = Column(Integer, nullable=False)
    security = Column(String, nullable=True)
    frequency = Column(Integer, nullable=True)
    label = Column(String, nullable=True)


all_AWARE_tables = [
    ESM,
    Location,
    Screen,
    LightSensor,
    Call,
    SMS,
    Application,
    Notification,
    Battery,
    WiFi,
    Proximity,
    Timezone,
    Processor,
    NetworkData,
    NetworkTraffic,
    Barometer,
    BarometerSensor,
    Temperature,
    TemperatureSensor,
    Bluetooth,
    Accelerometer,
    GoogleAR,
    Speech,
]

all_AWARE_table_names = [table.__tablename__ for table in all_AWARE_tables]


def increment_one(ii):
    return ii + 1


class AppCategories(Base):
    __tablename__ = "app_categories"
    id = Column(Integer, primary_key=True)
    application_name = Column(String, nullable=True)
    package_name = Column(String, nullable=False)
    package_hash = Column(String(64), index=True, nullable=False, unique=True)
    play_store_genre = Column(String, nullable=True)
    play_store_response = Column(SmallInteger, nullable=False)
    number_of_attempts = Column(
        SmallInteger, nullable=False, default=0, onupdate=increment_one
    )
    last_attempt = Column(
        TIMESTAMP(timezone=False),
        nullable=False,
        default=datetime.utcnow,
        onupdate=datetime.utcnow,
    )
