import base64
import json
import os
from datetime import datetime
from enum import Enum
from typing import List
from urllib.parse import urljoin

import pytz
import requests
from pygrocy.utils import parse_date, parse_float, parse_int
from tzlocal import get_localzone

DEFAULT_PORT_NUMBER=9192

class ShoppingListItem(object):
    def __init__(self, parsed_json):
        self._id = parse_int(parsed_json.get('id'))
        self._product_id = parse_int(parsed_json.get('product_id', None))
        self._note = parsed_json.get('note',None)
        self._amount = parse_float(parsed_json.get('amount'),0)
        self._row_created_timestamp = parse_date(parsed_json.get('row_created_timestamp', None))
        self._shopping_list_id = parse_int(parsed_json.get('shopping_list_id'))
        self._done = parse_int(parsed_json.get('done'))


    @property
    def id(self) -> int:
        return self._id
    
    @property
    def product_id(self) -> int:
        return self._product_id

    @property
    def note(self) -> str:
        return self._note

    @property
    def amount(self) -> float:
        return self._amount

class QuantityUnitData(object):
    def __init__(self, parsed_json):
        self._id = parse_int(parsed_json.get('id'))
        self._name = parsed_json.get('name')
        self._name_plural = parsed_json.get('name_plural')
        self._description = parsed_json.get('description')
        self._row_created_timestamp = parse_date(parsed_json.get('row_created_timestamp'))


class LocationData(object):
    def __init__(self, parsed_json):
        self._id = parse_int(parsed_json.get('id'))
        self._name = parsed_json.get('name')
        self._description = parsed_json.get('description')
        self._row_created_timestamp = parse_date(parsed_json.get('row_created_timestamp'))
        
    @property
    def id(self) -> int:
        return self._id
        
    @property
    def name(self) -> str:
        return self._name
        
    @property
    def description(self) -> str:
        return self._description


class ProductData(object):
    def __init__(self, parsed_json):
        self._id = parse_int(parsed_json.get('id'))
        self._name = parsed_json.get('name')
        self._description = parsed_json.get('description', None)
        self._location_id = parse_int(parsed_json.get('location_id', None))
        self._product_group_id = parse_int(parsed_json.get('product_group_id', None))
        self._qu_id_stock = parse_int(parsed_json.get('qu_id_stock', None))
        self._qu_id_purchase = parse_int(parsed_json.get('qu_id_purchsase', None))
        self._qu_factor_purchase_to_stock = parse_float(parsed_json.get('qu_factor_purchase_to_stock', None))
        self._picture_file_name = parsed_json.get('picture_file_name', None)
        self._allow_partial_units_in_stock = bool(parsed_json.get('allow_partial_units_in_stock', None) == "true")
        self._row_created_timestamp = parse_date(parsed_json.get('row_created_timestamp', None))
        self._min_stock_amount = parse_int(parsed_json.get('min_stock_amount', None), 0)
        self._default_best_before_days = parse_int(parsed_json.get('default_best_before_days', None))

        barcodes_raw = parsed_json.get('barcode', "")
        if barcodes_raw is None:
            self._barcodes = None
        else:
            self._barcodes = barcodes_raw.split(",")


    @property
    def id(self) -> int:
        return self._id
        
    @property
    def product_group_id(self) -> int:
        return self._product_group_id

    @property
    def name(self) -> str:
        return self._name

    @property
    def barcodes(self) -> List[str]:
        return self._barcodes


class ChoreData(object):
    def __init__(self, parsed_json):
        self._id = parse_int(parsed_json.get('id'))
        self._name = parsed_json.get('name')

    @property
    def id(self) -> int:
        return self._id

    @property
    def name(self) -> str:
        return self._name


class UserDto(object):
    def __init__(self, parsed_json):
        self._id = parse_int(parsed_json.get('id'))

        self._username = parsed_json.get('username')
        self._first_name = parsed_json.get('first_name')
        self._last_name = parsed_json.get('last_name')
        self._display_name = parsed_json.get('display_name')

    @property
    def id(self) -> int:
        return self._id

    @property
    def username(self) -> str:
        return self._username

    @property
    def first_name(self) -> str:
        return self._first_name

    @property
    def last_name(self) -> str:
        return self._last_name

    @property
    def display_name(self) -> str:
        return self._display_name


class CurrentChoreResponse(object):
    def __init__(self, parsed_json):
        self._chore_id = parse_int(parsed_json.get('chore_id'), None)
        self._last_tracked_time = parse_date(parsed_json.get('last_tracked_time'))
        self._next_estimated_execution_time = parse_date(parsed_json.get('next_estimated_execution_time'))

    @property
    def chore_id(self) -> int:
        return self._chore_id

    @property
    def last_tracked_time(self) -> datetime:
        return self._last_tracked_time

    @property
    def next_estimated_execution_time(self) -> datetime:
        return self._next_estimated_execution_time



class CurrentStockResponse(object):
    def __init__(self, parsed_json):
        self._product_id = parse_int(parsed_json.get('product_id'))
        self._amount = parse_float(parsed_json.get('amount'))
        self._best_before_date = parse_date(parsed_json.get('best_before_date'))
        self._amount_opened = parse_float(parsed_json.get('amount_opened'))
        
    @property
    def product_id(self) -> int:
        return self._product_id

    @property
    def amount(self) -> float:
        return self._amount

    @property
    def best_before_date(self) -> datetime:
        return self._best_before_date

    @property
    def amount_opened(self) -> float:
        return self._amount_opened

class MissingProductResponse(object):
    def __init__(self, parsed_json):
        self._product_id = parse_int(parsed_json.get('id'))
        self._name = parsed_json.get('name')
        self._amount_missing = parse_float(parsed_json.get('amount_missing'))
        self._is_partly_in_stock = bool(parse_int(parsed_json.get('is_partly_in_stock')))

    @property
    def product_id(self) -> int:
        return self._product_id

    @property
    def name(self) -> str:
        return self._name

    @property
    def amount_missing(self) -> float:
        return self._amount_missing

    @property
    def is_partly_in_stock(self) -> bool:
        return self._is_partly_in_stock

class CurrentVolatilStockResponse(object):
    def __init__(self, parsed_json):
        self._expiring_products = [CurrentStockResponse(product) for product in parsed_json.get('expiring_products')]
        self._expired_products = [CurrentStockResponse(product) for product in parsed_json.get('expired_products')]
        self._missing_products = [MissingProductResponse(product) for product in parsed_json.get('missing_products')]

    @property
    def expiring_products(self) -> List[CurrentStockResponse]:
        return self._expiring_products

    @property
    def expired_products(self) -> List[CurrentStockResponse]:
        return self._expired_products

    @property
    def missing_products(self) -> List[MissingProductResponse]:
        return self._missing_products



class ProductDetailsResponse(object):
    def __init__(self, parsed_json):
        self._last_purchased = parse_date(parsed_json.get('last_purchased'))
        self._last_used = parse_date(parsed_json.get('last_used'))
        self._stock_amount = parse_int(parsed_json.get('stock_amount'))
        self._stock_amount_opened = parse_int(parsed_json.get('stock_amount_opened'))
        self._next_best_before_date = parse_date(parsed_json.get('next_best_before_date'))
        self._last_price = parse_float(parsed_json.get('last_price'))

        self._product = ProductData(parsed_json.get('product'))

        self._quantity_unit_purchase = QuantityUnitData(parsed_json.get('quantity_unit_purchase'))
        self._quantity_unit_stock = QuantityUnitData(parsed_json.get('quantity_unit_stock'))

        self._location = LocationData(parsed_json.get('location'))

    @property
    def last_purchased(self) -> datetime:
        return self._last_purchased

    @property
    def last_used(self) -> datetime:
        return self._last_used

    @property
    def stock_amount(self) -> int:
        return self._stock_amount

    @property
    def stock_amount_opened(self) -> int:
        return self._stock_amount_opened

    @property
    def next_best_before_date(self) -> datetime:
        return self._next_best_before_date

    @property
    def last_price(self) -> float:
        return self._last_price

    @property
    def product(self) -> ProductData:
        return self._product


class ChoreDetailsResponse(object):
    def __init__(self, parsed_json):
        self._chore = ChoreData(parsed_json.get('chore'))
        self._last_tracked = parse_date(parsed_json.get('last_tracked'))

        if self._last_tracked is None:
            self._last_done_by = None
        else:
            self._last_done_by = UserDto(parsed_json.get('last_done_by'))

    @property
    def chore(self) -> ChoreData:
        return self._chore

    @property
    def last_done_by(self) -> UserDto:
        return self._last_done_by

    @property
    def last_tracked(self) -> datetime:
        return self._last_tracked


class TransactionType(Enum):
    PURCHASE = "purchase"
    CONSUME = "consume"
    INVENTORY_CORRECTION = "inventory-correction"
    PRODUCT_OPENED = "product-opened"


class GrocyApiClient(object):
    def __init__(self, base_url, api_key, port: int = DEFAULT_PORT_NUMBER, verify_ssl = True):
        self._base_url = '{}:{}/api/'.format(base_url, port)
        self._api_key = api_key
        self._verify_ssl = verify_ssl
        if self._api_key == "demo_mode":
            self._headers = { "accept": "application/json" }
        else:
            self._headers = {
                "accept": "application/json",
                "GROCY-API-KEY": api_key
            }

    def _do_get_request(self, end_url: str):
        req_url = urljoin(self._base_url, end_url)
        resp = requests.get(
            req_url, verify=self._verify_ssl, headers=self._headers)
        resp.raise_for_status()
        if len(resp.content) > 0:
            return resp.json()

    def _do_post_request(self, end_url: str, data: dict):
        req_url = urljoin(self._base_url, end_url)
        resp = requests.post(
            req_url, verify=self._verify_ssl,
            headers=self._headers,
            data=data)
        resp.raise_for_status()
        if len(resp.content) > 0:
            return resp.json()

    def _do_put_request(self, end_url: str, data):
        req_url = urljoin(self._base_url, end_url)
        up_header = self._headers.copy()
        up_header['accept'] = '*/*'
        if isinstance(data, dict):
            up_header['Content-Type'] = 'application/json'
            data = json.dumps(data)
        else:
            up_header['Content-Type'] = 'application/octet-stream'
        resp = requests.put(
            req_url, verify=self._verify_ssl,
            headers=up_header,
            data=data)
        resp.raise_for_status()
        if len(resp.content) > 0:
            return resp.json()

    def get_stock(self) -> List[CurrentStockResponse]:
        parsed_json = self._do_get_request("stock")
        return [CurrentStockResponse(response) for response in parsed_json]

    def get_volatile_stock(self) -> CurrentVolatilStockResponse:
        parsed_json = self._do_get_request("stock/volatile")
        return CurrentVolatilStockResponse(parsed_json)

    def get_product(self, product_id) -> ProductDetailsResponse:
        url = f"stock/products/{product_id}"
        parsed_json = self._do_get_request(url)
        if parsed_json:
            return ProductDetailsResponse(parsed_json)

    def get_chores(self) -> List[CurrentChoreResponse]:
        parsed_json = self._do_get_request("chores")
        return [CurrentChoreResponse(chore) for chore in parsed_json]

    def get_chore(self, chore_id: int) -> ChoreDetailsResponse:
        url = f"chores/{chore_id}"
        parsed_json = self._do_get_request(url)
        if parsed_json:
            return ChoreDetailsResponse(parsed_json)

    def execute_chore(self, chore_id: int, done_by: int = None, tracked_time: datetime = datetime.now()):
        # Grocy API expects UTC time; time returned from datetime.now() is local time without timezone
        # information, so timezone information must be attached.
        local_tz = get_localzone()
        localized_tracked_time = local_tz.localize(tracked_time)

        data = {
            "tracked_time": localized_tracked_time.isoformat()
        }

        if done_by is not None:
            data["done_by"] = done_by

        return self._do_post_request(f"chores/{chore_id}/execute", data)

    def add_product(self, product_id, amount: float, price: float, best_before_date: datetime = None,
                    transaction_type: TransactionType = TransactionType.PURCHASE):
        data = {
            "amount": amount,
            "transaction_type": transaction_type.value,
            "price": price
        }

        if best_before_date is not None:
            data["best_before_date"] = best_before_date.strftime('%Y-%m-%d')

        return self._do_post_request(f"stock/products/{product_id}/add", data)

    def consume_product(self, product_id: int, amount: float = 1, spoiled: bool = False,
                        transaction_type: TransactionType = TransactionType.CONSUME):
        data = {
            "amount": amount,
            "spoiled": spoiled,
            "transaction_type": transaction_type.value
        }

        self._do_post_request(f"stock/products/{product_id}/consume", data)
        
    def get_shopping_list(self) -> List[ShoppingListItem]:
        parsed_json = self._do_get_request("objects/shopping_list")
        return [ShoppingListItem(response) for response in parsed_json]

    def add_missing_product_to_shopping_list(self, shopping_list_id: int = 1):
        data = {
            "list_id": shopping_list_id
        }

        self._do_post_request("stock/shoppinglist/add-missing-products", data)
    
    def add_product_to_shopping_list(self, product_id: int, shopping_list_id: int = 1, amount: int = 1):
        data = {
            "product_id": product_id,
            "list_id": shopping_list_id,
            "product_amount": amount
        }
        self._do_post_request("stock/shoppinglist/add-product", data)
    
    def clear_shopping_list(self, shopping_list_id: int = 1):
        data = {
            "list_id": shopping_list_id
        }

        self._do_post_request("stock/shoppinglist/clear", data)
            
    def remove_product_in_shopping_list(self, product_id: int, shopping_list_id: int = 1, amount: int = 1):
        data = {
            "product_id": product_id,
            "list_id": shopping_list_id,
            "product_amount": amount
        }
        self._do_post_request("stock/shoppinglist/remove-product", data)
        
    def get_product_groups(self) -> List[LocationData]:
        parsed_json = self._do_get_request("objects/product_groups")
        return [LocationData(response) for response in parsed_json]

    def upload_product_picture(self, product_id: int, pic_path: str):
        b64fn = base64.b64encode('{}.jpg'.format(product_id).encode('ascii'))
        req_url = "files/productpictures/" + str(b64fn, "utf-8")
        with open(pic_path,'rb') as pic:
            self._do_put_request(req_url, pic)
            
    def update_product_pic(self, product_id: int):
        pic_name = f"{product_id}.jpg"
        data = { "picture_file_name":  pic_name }
        self._do_put_request(f"objects/products/{product_id}", data)
            
    def get_userfields(self, entity: str, object_id: int):
        url = f"userfields/{entity}/{object_id}"
        return self._do_get_request(url)
        
    def set_userfields(self, entity: str, object_id: int, key: str, value):
        data = {
            key: value
        }
        self._do_put_request(f"userfields/{entity}/{object_id}", data)

    def get_last_db_changed(self):
        resp = self._do_get_request("system/db-changed-time")
        last_change_timestamp = parse_date(resp.get('changed_time'))
        return last_change_timestamp
