from collections import defaultdict
from typing import List, Tuple
from django.shortcuts import get_object_or_404
from django.urls import reverse
from ninja import ModelSchema, Router, Schema, Field

from django.db import transaction, IntegrityError

# from .decorators import user_is_author_or_rapid_checker
from django.core.exceptions import PermissionDenied
import pydicom
from pydicom.errors import InvalidDicomError

from generic.decorators import check_user_in_group
from generic.constants import Group

from ninja.security import django_auth

from ninja import NinjaAPI, File
from ninja.files import UploadedFile

from generic.models import Examination, Modality

from .models import Case, DuplicateDicom, Series, SeriesImage, UncategorisedDicom

from loguru import logger


router = Router()


class SeriesSchema(ModelSchema):
    case_id: List[int] = []

    class Meta:
        model = Series
        fields = [
            "id",
            "modality",
            "examination",
            "series_instance_uid",
            "author",
        ]

    @staticmethod
    def resolve_case_id(obj):
        return [i.id for i in obj.case.all()]


class CaseSchema(ModelSchema):
    class Meta:
        model = Case
        fields = ["id", "title"]


@router.post("/upload_dicom", auth=django_auth)
def upload_dicom(request, files: List[UploadedFile] = File(...)):
    uploaded = []
    duplicate = []
    failed = []
    duplicate_series = set()
    for file in files:
        # data = file.read()
        try:
            ud = UncategorisedDicom(image=file, user=request.user)

            ud.save()

            uploaded.append((file.name, ud.image_blake3_hash))
        except DuplicateDicom as e:
            duplicate.append((file.name, ud.image_blake3_hash))


            if type(e.duplicate) == SeriesImage:
                duplicate_series.add(e.duplicate.series.get_absolute_url())

            pass
        except InvalidDicomError:
            failed.append(file.name)

    return {
        "uploaded": uploaded,
        "duplicates": duplicate,
        "failed": failed,
        "duplicate_series": list(duplicate_series),
    }


@router.post("/generate_image_hash", auth=django_auth)
def generate_image_hash(request, id: int):
    s = SeriesImage.objects.get(pk=id)
    s.generate_hashes()

    return {
        "md5": s.image_md5_hash,
        "blake3": s.image_blake3_hash,
        "is_dicom": s.is_dicom,
    }


@router.post("/clear_dicoms", auth=django_auth)
def clear_dicoms(request):
    if "selection" in request.POST:
        dicoms = UncategorisedDicom.objects.filter(
            series_instance_uid__in=request.POST.getlist("selection")
        )

    else:
        dicoms = UncategorisedDicom.objects.filter(user=request.user)

    dicoms.delete()

    return True


@router.get("/uncategorised_dicoms", auth=django_auth)
def uncategorised_dicoms(request):
    dicoms = UncategorisedDicom.objects.filter(user=request.user)

    data = defaultdict(list)
    for d in dicoms:
        tags = d.get_basic_dicom_tags()

        data[tags["SeriesInstanceUID"]].append(tags)
        data["image_hash"].append(d.image_blake3_hash)

    return data


def import_dicoms_helper(request, case_id: int | None = None):
    if "selection" in request.POST:
        dicoms = UncategorisedDicom.objects.filter(
            series_instance_uid__in=request.POST.getlist("selection")
        )
    else:
        dicoms = UncategorisedDicom.objects.filter(user=request.user)

    if "order-series" in request.POST:
        order = request.POST["order-series"]
    else:
        order = None

    # Group dicoms by SeriesInstanceUID, but don't keep all tags in memory
    series_map = {}
    for d in dicoms.iterator():  # Use iterator() to avoid loading all at once
        tags = d.basic_dicom_tags
        series_uid = tags["SeriesInstanceUID"]

        if series_uid not in series_map:
            # Get or create Series and related objects as needed
            try:
                modality = Modality.objects.get(short_code=tags["Modality"])
            except Modality.DoesNotExist:
                modality = Modality.objects.create(short_code=tags["Modality"], modality=tags["Modality"])

            series_tags = tags  # Save the first tag for this series
            try:
                with transaction.atomic():
                    series, created = Series.objects.get_or_create(
                        series_instance_uid=series_uid,
                        defaults={
                            "modality": modality,
                            "description": tags.get("SeriesDescription", ""),
                        }
                    )
                    if created and tags.get("StudyDescription"):
                        examination, created_exam = Examination.objects.get_or_create(
                            examination=tags["StudyDescription"]
                        )
                        if created_exam:
                            examination.modality = modality
                            examination.save()
                        series.examination = examination
                        series.save()
            except IntegrityError:
                series = Series.objects.get(series_instance_uid=series_uid)

            # Add author and case if needed
            series.author.add(request.user)
            if case_id is not None:
                case_object = get_object_or_404(Case, pk=case_id)
                try:
                    series.case.add(case_object)
                except IntegrityError:
                    pass

            series_map[series_uid] = series
        else:
            series = series_map[series_uid]

        # Create and save SeriesImage immediately
        series_image = SeriesImage(
            image=d.image,
            series=series,
            is_dicom=True,
            image_blake3_hash=d.image_blake3_hash,
        )
        series_image.save()
        d.delete()  # Remove UncategorisedDicom immediately

    # Optionally, re-order series if needed
    for series in series_map.values():
        match order:
            case "order-series-instance-number":
                series.order_by_dicom("InstanceNumber")
            case "order-series-slice-location":
                series.order_by_dicom("SliceLocation")
            case _:
                pass

    return [(series, series.get_link()) for series in series_map.values()]


@router.post(
    "/import_dicoms", auth=django_auth, response=List[Tuple[SeriesSchema, str]]
)
def import_dicoms(request):
    return import_dicoms_helper(request)


@router.post(
    "/import_dicoms/{case_id}",
    auth=django_auth,
    response=List[Tuple[SeriesSchema, str]],
)
def import_dicoms_case(request, case_id: int):
    return import_dicoms_helper(request, case_id=case_id)


@router.get("/orphan_series", auth=django_auth, response=List[SeriesSchema])
def orphan_series(request):
    return request.user.series.filter(case=None)


@router.get("/series_remove_duplicate_images", auth=django_auth)
def series_remove_duplicate_images(request, series_id: int):
    series = get_object_or_404(Series, pk=series_id)

    img_ids = set()
    dupes = set()
    for series_image in series.images.all():
        if series_image.image_blake3_hash in img_ids:
            dupes.add(series_image.id)
        img_ids.add(series_image.image_blake3_hash)

    if dupes:
        SeriesImage.objects.filter(id__in=dupes).delete()

    return len(dupes)


@router.get("/series_truncate/{series_id}/{start}/{end}/", auth=django_auth)
def series_truncate(request, series_id: int, start: int, end: int):
    series = get_object_or_404(Series, pk=series_id)

    if not series.check_user_can_edit(request.user):
        return {"status": "permission denied"}

    images_removed = []
    for n, image in enumerate(series.get_images()):
        if n >= start and n <= end:
            continue
        else:
            image.removed = True
            image.image = None
            image.save()
            images_removed.append(image.pk)
    
    series.get_total_image_size(refresh=True)

    return {"status": "success", "images_removed": images_removed}


@router.get("/get_cases_user", auth=django_auth, response=List[CaseSchema])
def get_cases_user(request):
    return Case.objects.filter(author=request.user)


@router.get("/check_image_hash/{hash}", auth=django_auth)
def check_image_hash(request, hash: str):
    try:
        series_image = SeriesImage.objects.get(image_blake3_hash=hash)
        data = {
            "status": "success",
            "id": series_image.pk,
            "url": series_image.series.get_absolute_url(),
        }
        return data
    except SeriesImage.DoesNotExist:
        data = {"status": "success", "id": False}
        return data


@router.post("/check_image_hashes/", auth=django_auth)
def check_images_hashes(request, hashes: List[str]):
    """Checks a list of image hashes and returns the series id / url if found

    Return format
    { "hash_id": {"id": "series_id|false", "url": "series_url|false"}, ...}
    """

    hash_status = {}

    for hash in hashes:
        try:
            # TOOD also check against uncategorised dicoms?
            series_image = SeriesImage.objects.get(image_blake3_hash=hash)
            data = {
                "id": series_image.pk,
                "url": series_image.series.get_absolute_url(),
                "type": "series",
            }
        except SeriesImage.DoesNotExist:
            try:
                uncategorised_dicom = UncategorisedDicom.objects.get(
                    image_blake3_hash=hash
                )
                data = {
                    "id": uncategorised_dicom.pk,
                    "url": reverse("atlas:user_uploads"),
                    "type": "uncategorised",
                }
            except UncategorisedDicom.DoesNotExist:
                data = {"id": False, "url": False}

        hash_status[hash] = data

    return hash_status


# @router.get("/generate_image_hash/{id}", auth=django_auth)
# def generate_image_hash(request, id: int):
#        series_image = SeriesImage.objects.get(pk=id)
#
#        series_image.image_blake3_hash = ""
#
#        series_image.save()
#
#        print(series_image)


@router.get("/view_dicom_tags/{hash}", auth=django_auth)
def view_dicom_tags(request, hash: str):
    item = SeriesImage.objects.get(image_blake3_hash=hash)
    return item.get_dicom_json()


@router.get("/series_split_by_dicom_tag/{series_id}/{dicom_tag}", auth=django_auth)
def series_split_by_tag(request, series_id: int, dicom_tag: str):
    series = get_object_or_404(Series, pk=series_id)

    if not series.check_user_can_edit(request.user):
        return {"status": "permission denied"}

    if dicom_tag.startswith("(") and dicom_tag.endswith(")"):
        dicom_tag = tuple([hex(int(i.strip(), 16)) for i in dicom_tag[1:-1].split(",")])

    image_map = defaultdict(list)

    # Use iterator to avoid loading all images into memory
    for image in series.images.iterator():
        ds = image.get_dicom_data()
        if dicom_tag in ds:
            val = ds[dicom_tag].value
            if isinstance(val, pydicom.multival.MultiValue):
                key = ",".join(map(str, val))
            else:
                key = str(val)
            image_map[key].append(image)
        else:
            return [f"{dicom_tag} not found {image}"]

    case = list(series.case.all())
    authors = list(series.author.all())
    new_series = []

    # Use a transaction for all DB changes
    with transaction.atomic():
        for n, (option, images) in enumerate(image_map.items()):
            if n == 0:
                # Update the original series
                series.images.set(images)
                new_series.append(series.pk)
            else:
                # Clone the series
                series.pk = None
                series.save()
                series.case.set(case)
                series.author.set(authors)
                series.images.set(images)
                new_series.append(series.pk)

    return new_series


@router.get("/split_order_by_dicom_tag/{series_id}/{dicom_tag}", auth=django_auth)
def series_order_by_tag(request, series_id: int, dicom_tag: str):
    series = get_object_or_404(Series, pk=series_id)

    series.order_by_dicom(dicom_tag)

    return [f"{series} ordered by {dicom_tag}"]
