# note-to-self
# like common, but can import models
import importlib.util
import json
import os
import time
from dataclasses import dataclass, asdict
from pathlib import Path

from starlette.staticfiles import StaticFiles

from otree import settings
from otree.database import db
from otree.models_concrete import PageTimeBatch
from otree.currency import Currency, RealWorldCurrency
from otree.decimal import DecimalValue

import typing


@dataclass
class TimeSpentRow:
    session_code: str
    participant_id_in_session: int
    participant_code: str
    page_index: int
    app_name: str
    page_name: str
    epoch_time_completed: int
    round_number: int
    timeout_happened: int
    is_wait_page: int


page_completion_buffer = []
page_completion_last_write = 0

BUFFER_SIZE = 50


def write_row_to_page_buffer(row: TimeSpentRow):
    d = asdict(row)
    row = ','.join(map(str, d.values())) + '\n'

    page_completion_buffer.append(row)
    if (
        len(page_completion_buffer) > BUFFER_SIZE
        or time.time() - page_completion_last_write > 60 * 2
    ):
        write_page_completion_buffer()


def make_page_completion_row(
    *,
    view,
    app_name,
    participant__id_in_session,
    participant__code,
    session_code,
    is_wait_page,
):
    now = int(time.time())
    row = TimeSpentRow(
        app_name=app_name,
        page_index=view._index_in_pages,
        page_name=type(view).__name__,
        epoch_time_completed=now,
        round_number=view.round_number,
        participant_id_in_session=participant__id_in_session,
        participant_code=participant__code,
        session_code=session_code,
        timeout_happened=int(bool(getattr(view, 'timeout_happened', False))),
        is_wait_page=is_wait_page,
    )
    write_row_to_page_buffer(row)


def write_page_completion_buffer():
    global page_completion_last_write
    db.add(PageTimeBatch(text=''.join(page_completion_buffer)))
    page_completion_last_write = time.time()
    page_completion_buffer.clear()


class OTreeStaticFiles(StaticFiles):
    # copied from starlette, just to change 'statics' to 'static',
    # and to fail silently if the dir does not exist.
    def get_directories(self, directory, packages):
        directories = []
        if directory is not None:
            directories.append(directory)

        for package in packages or []:
            spec = importlib.util.find_spec(package)
            assert (
                spec is not None and spec.origin is not None
            ), f"Package {package!r} could not be found, or maybe __init__.py is missing"
            package_directory = os.path.normpath(
                os.path.join(spec.origin, "..", "static")
            )
            if os.path.isdir(package_directory):
                directories.append(package_directory)

        return directories

    def assert_file_exists(self, path):
        if path in existing_filenames_cache:
            return
        for _dir in self.all_directories:
            if Path(_dir, path).is_file():
                existing_filenames_cache.add(path)
                return
        raise FileNotFoundError(path)

    def urls_of_static_files(self, dirpath, extension):
        from otree.asgi import app

        if extension.startswith('.'):
            raise Exception("Extension should not contain a dot")

        for root in self.all_directories:
            _dir = Path(root, dirpath)
            if _dir.is_dir():
                for _file in _dir.glob(f'*.{extension}'):
                    relpath = _file.relative_to(root)
                    yield app.router.url_path_for('static', path=relpath)


existing_filenames_cache = set()


def url_of_static_file(path):
    """
    - better than hardcoding '/static/', which will fail silently if the file
    doesn't exist.
    - there is the {{ static }} tag in templates.
      good to have this in python code for consistency,
      rather than making people construct it manually.
    - useful for:
        - Trials, which are generated on the server side.
          this would be useful for 2 situations:
        - live pages, where {% static %} can't be used because the template was already rendered
        - js_vars (don't want {% static %} mixed in with JS code)

    naming:
    - it shouldn't start with
    'static' because that would distract from @staticmethod in autocomplete,
    which is much more important.
    - url_of_static is more specific than url_for_static (which looks like vars_for_template but works differently)

    """
    from otree.asgi import app

    static_files_app.assert_file_exists(path)
    return app.router.url_path_for('static', path=path)


def urls_of_static_files(dir, extension):
    """
    e.g.:
    >>> urls_of_static_files('emojis', 'png')
    ['/static/emojis/1.png', '/static/emojis/2.png']

    I don't know how useful this would be because usually people have (a) some metadata about the file,
    such as what it contains. also for DB storage & record-keeping,
    people would want to store the filename, e.g. '1.png',
    rather than the full URL path, e.g. '/static/emojis/1.png'

    But if we see a lot of people just trying to include all images in a dir,
    then we can add this to API.
    """
    return static_files_app.urls_of_static_files(dir, extension)


static_files_app = OTreeStaticFiles(
    directory='_static', packages=['otree'] + settings.OTREE_APPS
)


class OTreeJSONEncoder(json.JSONEncoder):
    """JSON encoder that handles Currency and DecimalValue types"""

    def default(self, obj):

        if isinstance(obj, (Currency, RealWorldCurrency)):
            if obj.get_num_decimal_places() == 0:
                return int(obj)
            return float(obj)
        if isinstance(obj, DecimalValue):
            return float(obj)
        # Let the base class default method raise the TypeError
        return json.JSONEncoder.default(self, obj)


def json_dumps(obj):
    """JSON dumps with oTree type support"""
    return json.dumps(obj, cls=OTreeJSONEncoder)


safe_json = json_dumps
