import datetime
import json
import os
import pathlib
import random
from collections import namedtuple
from pathlib import Path
from typing import List, Literal
from uuid import uuid4

import requests
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.files.storage import default_storage
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import lorem_ipsum
from django.utils.text import capfirst
from wagtail.documents.models import Document
from wagtail.images import get_image_model

Image = get_image_model()


n = random.randint


def rbool(prob=0.5):
    return random.random() < prob


def lpar(num):
    """``num`` random paragraphs of lorem ipsum."""
    return "".join("<p>{0}</p>".format(p) for p in lorem_ipsum.paragraphs(num, False))


def lwords(num):
    """``num`` random lorem ipsum words."""
    return capfirst(lorem_ipsum.words(num, False))


def tel():
    """random international format (aus) telephone number eg. +61 (0) 412 345 678"""
    return "+{0} ({1}) {2} {3} {4}".format(
        n(10, 99), n(0, 9), n(100, 999), n(100, 999), n(100, 999)
    )


def rdate(future=True):
    """
    A random date. Set ``future`` to True for dates in the future, False for
    dates in the past. Possible dates span up to five years from today.
    """
    now = datetime.date.today()
    delta = datetime.timedelta(days=n(0, 1825))
    if future:
        return now + delta
    else:
        return now - delta


def r_heading(level, num):
    return "<h{0}>{1}</h{0}>".format(level, lwords(num))


def r_bold(num):
    return "<p><b>{0}</b></p>".format(lwords(num))


def r_italic(num):
    return "<p><i>{0}</i></p>".format(lwords(num))


def r_ordered_list(list_elements):
    list_elems = []
    for _ in range(list_elements):
        list_elems.append("<li>{0}</li>\n".format(lwords(n(2, 4))))
    return "<ol>\n{0}</ol>".format("\n".join(map(str, list_elems)))


def r_unordered_list(list_elements):
    list_elems = []
    for _ in range(list_elements):
        list_elems.append("<li>{0}</li>\n".format(lwords(n(2, 4))))
    return "<ul>\n{0}</ul>".format("\n".join(map(str, list_elems)))


def hr():
    return "<hr>"


def r_anchor(num):
    return "<a href='http://example.com'>{0}</a>".format(lwords(num))


def r_image():
    image = get_random_image()
    return '<embed alt="Example image" embedtype="image" format="fullwidth" id="{0}"/>'.format(
        image.id
    )


def embed_video():
    return '<embed embedtype="media" url="{0}"/>'.format(get_random_video_url())


def rich_text_example():
    """Layout all possible elements that can be used in rich text fields."""
    return open(os.path.join(os.path.dirname(__file__), "rich_text_example.html"), "r").read()


def random_rich_text_example(num, embeds=False):
    """
    Layout some possible elements that can be used in rich text fields in a random fashion
    Takes one argument, the number of elements. Set embeds=True if you wish to include images
    and videos
    """
    funcs = [
        lambda: r_heading(n(2, 5), n(1, 3)),
        lambda: r_bold(n(3, 5)),
        lambda: r_italic(n(3, 5)),
        lambda: r_ordered_list(n(3, 5)),
        lambda: r_unordered_list(n(3, 5)),
        lambda: r_anchor(n(3, 5)),
        lambda: lpar(n(3, 5)),
        hr,
    ]
    if embeds:
        funcs.extend([r_image, embed_video])
    return "\n".join([random.choice(funcs)() for _ in range(num)])


def json_dumps(x):
    """Dump a value to JSON using the Django JSON encoder"""
    return json.dumps(x, cls=DjangoJSONEncoder)


def shuf(l1):
    """Generate a shuffled copy of a list"""
    l2 = l1[:]
    random.shuffle(l2)
    return l2


class once(object):
    """A decorator that only calls the decorated function once"""

    has_run = False
    value = None
    fn = None

    def __init__(self, fn):
        self.fn = fn

    def __call__(self, *args, **kwargs):
        if self.has_run:
            return self.value

        self.value = self.fn(*args, **kwargs)
        self.has_run = True
        return self.value


def setup_images(test_img_dir=None, tag=None):
    def _is_image(filename):
        extensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
        return pathlib.Path(filename).suffix.lower() in extensions

    image_prefix = "original_images/test-images"

    if test_img_dir is None:
        HERE = os.path.join(os.path.dirname(__file__))
        test_img_dir = os.path.join(HERE, "test_images")
    CopiedImage = namedtuple("CopiedImage", ["name", "path"])
    copied_images: List[CopiedImage] = []
    # Copy all images from test_img_dir to assets directory
    for filename in os.listdir(test_img_dir):
        image_src_path = os.path.join(test_img_dir, filename)
        image_dest_path = os.path.join(image_prefix, filename)
        if not default_storage.exists(image_dest_path) and _is_image(filename):
            with open(image_src_path, "rb") as f:
                default_storage.save(image_dest_path, f)
        copied_images.append(CopiedImage(name=filename, path=image_dest_path))
    # Create Image models for each image that was found
    for i in copied_images:
        if _is_image(i.path):
            image_obj = Image.objects.create(title=i.name, file=i.path)
            if tag:
                image_obj.tags.add(tag)


def get_random_image(category: str = None) -> Image:
    images = Image.objects.order_by("?")
    if category:
        images = images.filter(tags__name__in=[category])
    return images.first()


def get_image(filename):
    return Image.objects.get(title=filename)


def setup_documents(test_document_dir=None):
    dest_dir = Path(settings.MEDIA_ROOT, "documents")

    if test_document_dir is None:
        here = Path(__file__).parent.absolute()
        test_document_dir = here.joinpath("test_documents")
    for src_file in test_document_dir.iterdir():
        doc_dest_path = dest_dir.joinpath(
            src_file.name
        ).as_posix()  # default_storage needs a string
        if not default_storage.exists(doc_dest_path) and src_file.is_file():
            with open(src_file.as_posix(), "rb") as doc_file:
                default_storage.save(doc_dest_path, doc_file)
    for file in dest_dir.iterdir():
        if file.is_file():
            Document.objects.create(title=file.name, file=file)


def get_random_doc():
    return Document.objects.order_by("?").first()


def get_random_video_url():
    return random.choice(
        [
            "https://youtu.be/H9VVkwRb_7M",
            "https://youtu.be/2WWwkArWajM",
            "https://youtu.be/yYaJWO_mNWs",
            "https://youtu.be/1kThvYlo2Lc",
            "https://youtu.be/Ji9qSuQapFY",
            "https://vimeo.com/155536745",
            "https://vimeo.com/179936131",
            "https://vimeo.com/188197250",
            "https://vimeo.com/175535505",
            "https://vimeo.com/188321226",
        ]
    )


def make_superuser():
    User = get_user_model()
    if User.USERNAME_FIELD == "email":
        User.objects.create_superuser(email="admin@admin.com", password="p")
    else:
        User.objects.create_superuser(username="admin", email="admin@admin.com", password="p")


def generate_form_streamfield(headings=True, file_field=True):
    """Generates one field of each possible type"""
    basic_types = ["text", "textarea", "email", "number", "date"]

    if headings:
        yield ("heading", "Basic Types")
    for field_type in basic_types:
        yield (
            "basic_field",
            {
                "label": field_type,
                "field_type": field_type,
                "help_text": lwords(10),
                "required": rbool(),
                "default_value": "",
                "placeholder": lwords(2),
            },
        )

    if headings:
        yield ("heading", "Choice Types")

    choice_types = ["select", "checkbox", "radio"]

    for field_type in choice_types:
        yield (
            "choice_field",
            {
                "label": field_type,
                "field_type": field_type,
                "choices": [lwords(n(1, 2)) for _ in range(n(2, 8))],
                "help_text": lwords(10),
                "required": rbool(),
            },
        )

    if file_field:
        if headings:
            yield ("heading", "File types")
        yield (
            "file_field",
            {
                "label": "File",
                "help_text": lwords(10),
                "required": rbool(),
            },
        )


def download_random_images(pic_type: Literal["people", "lorem"], limit=20):
    """
    Download some random images, if they exist, load them into the database.

    To use the images in factories pair with FuzzyImage(tag=pic_type)
    """
    path = Path(settings.MEDIA_ROOT, pic_type)
    if not path.exists():
        path.mkdir()
    # If directory is empty, create some test images
    if not list(path.iterdir()):
        for i in range(limit):
            if pic_type == "people":
                url = "https://thispersondoesnotexist.com"
            else:
                dimensions = [200, 300, 400, 500, 600]
                url = f"https://picsum.photos/{random.choice(dimensions)}/{random.choice(dimensions)}?random={i}"

            response = requests.get(url)
            image_file = path / f"{uuid4()}.jpg"
            with open(image_file, "wb") as f:
                f.write(response.content)

                try:
                    i = Image.objects.create(
                        file=f.name,
                    )
                    i.tags.add(pic_type)
                    print(f"Created image {image_file}")
                except Exception:
                    print("Image failed, deleting")
                    image_file.unlink()
    else:
        for image_file in path.iterdir():
            with open(image_file, "rb") as f:
                i = Image.objects.create(file=f.name)
                i.tags.add(pic_type)
