from fastapi import APIRouter, Query, HTTPException, Depends, Response, Request
import hashlib
import asyncio
from pydantic import BaseModel, Field
import zipfile
import io
import logging
import sys
import os
import time
import shutil

logger = logging.getLogger("sampacs")
from typing import List, Dict, Optional
import datetime
from fastapi.responses import StreamingResponse, FileResponse
from dbmodel import Study, Patient, Series, Instance
from utils import getImage
from config import config
import peewee
from peewee import fn
from pydantic import BaseModel
from ..dependencies import get_db

class StudyListResponse(BaseModel):
    """Study list item response model"""
    uid: str = Field(description="Study Instance UID")
    description: Optional[str] = Field(description="Study description")
    date: str = Field(description="Study date and time in ISO format")
    creator_aetitle: Optional[str] = Field(description="AETitle of the modality that created the study")
    patient: Optional[str] = Field(description="Patient name")


class StudyMainDicomTags(BaseModel):
    """Main DICOM tags for study"""
    institution_name: str = Field(default="", description="Institution name")
    study_date: str = Field(description="Study date in YYYYMMDD format")
    study_id: Optional[str] = Field(description="Study ID")
    study_instance_uid: str = Field(description="Study Instance UID")
    study_time: str = Field(description="Study time in HHMMSS format")


class PatientMainDicomTags(BaseModel):
    """Main DICOM tags for patient"""
    patient_birth_date: str = Field(description="Patient birth date in YYYYMMDD format")
    patient_id: str = Field(description="Patient ID")
    patient_name: str = Field(description="Patient name in DICOM format (LAST^FIRST^MIDDLE)")
    patient_sex: str = Field(description="Patient sex (M/F/O)")


class SeriesItem(BaseModel):
    """Series information item"""
    uid: str = Field(description="Series Instance UID")
    description: Optional[str] = Field(description="Series description")
    modality: str = Field(description="Series modality (CT, MR, US, etc.)")


# Утилиты для работы с ZIP архивами
def get_zip_base_path() -> str:
    """Получить базовый путь для хранения ZIP архивов"""
    return os.path.join(config.storageDir, "zip")


def get_today_zip_path() -> str:
    """Получить путь к папке с архивами текущей даты"""
    base_path = get_zip_base_path()
    today = datetime.datetime.now().strftime("%Y-%m-%d")
    return os.path.join(base_path, today)


def cleanup_old_zip_folders():
    """Удалить папки с архивами старше текущей даты"""
    base_path = get_zip_base_path()
    if not os.path.exists(base_path):
        return

    today = datetime.datetime.now().strftime("%Y-%m-%d")
    for item in os.listdir(base_path):
        item_path = os.path.join(base_path, item)
        if os.path.isdir(item_path) and item != today:
            try:
                shutil.rmtree(item_path)
                logger.info(f"Удалена старая папка с архивами: {item}")
            except Exception as e:
                logger.error(f"Ошибка при удалении папки {item}: {e}")


def is_lock_expired(lock_path: str, timeout_minutes: int = 30) -> bool:
    """
    Проверяет, истекло ли время lock файла.

    Args:
        lock_path: путь к lock файлу
        timeout_minutes: время жизни lock в минутах

    Returns:
        True если lock просрочен или файл поврежден
    """
    try:
        if not os.path.exists(lock_path):
            return True

        with open(lock_path, 'r') as f:
            content = f.read().strip()
            if not content:
                return True
            timestamp = float(content)

        current_time = time.time()
        lock_age_minutes = (current_time - timestamp) / 60
        return lock_age_minutes > timeout_minutes

    except (OSError, ValueError, TypeError):
        # Если не можем прочитать файл - считаем просроченным
        return True


def create_lock_file(lock_path: str):
    """Создать lock файл с текущим timestamp"""
    os.makedirs(os.path.dirname(lock_path), exist_ok=True)
    with open(lock_path, 'w') as f:
        f.write(str(time.time()))


def remove_lock_file(lock_path: str):
    """Удалить lock файл"""
    try:
        if os.path.exists(lock_path):
            os.remove(lock_path)
    except OSError:
        pass


def get_zip_file_path(study_uid: str) -> str:
    """Получить путь к ZIP файлу исследования"""
    zip_dir = get_today_zip_path()
    return os.path.join(zip_dir, f"{study_uid}.zip")


def get_lock_file_path(study_uid: str) -> str:
    """Получить путь к lock файлу исследования"""
    zip_dir = get_today_zip_path()
    return os.path.join(zip_dir, f"{study_uid}.lock")



class StudyDetailResponse(BaseModel):
    """Detailed study information response model"""
    study_uid: str = Field(description="Study Instance UID")
    description: str = Field(decription="Study description")
    is_stable: bool = Field(description="Whether the study is stable (not being modified)")
    main_dicom_tags: StudyMainDicomTags = Field(description="Main DICOM tags for the study")
    patient_main_dicom_tags: PatientMainDicomTags = Field(description="Main DICOM tags for the patient")
    series: List[SeriesItem] = Field(description="List of series in this study")


router = APIRouter(prefix="/api/studies", tags=["studies"])

@router.get("", summary="Получить список исследований", response_description="Список исследований")
async def get_studies(
    from_date: Optional[str] = Query(None, description="Start date in YYYY-MM-DD format"),
    till_date: Optional[str] = Query(None, description="End date in YYYY-MM-DD format"),
    aetitle: Optional[str] = Query(None, description="Filter by creator AETitle (multiple values separated by comma)"),
    patient: Optional[str] = Query(None, description="Filter by patient name"),
    patient_id: Optional[str] = Query(None, description="Filter by patient id. Ignore other filters"),
    db: None = Depends(get_db)
) -> List[StudyListResponse]:
    """
    Получить список исследований (studies) из PACS системы.

    ## Что такое Study в DICOM?

    Study (исследование) - это основной организационный уровень в DICOM иерархии,
    представляющий полное медицинское исследование пациента. Каждое исследование:

    - Имеет уникальный Study Instance UID
    - Содержит одну или несколько Series (серий)
    - Принадлежит одному Patient (пациенту)
    - Может содержать снимки разных модальностей (КТ, МРТ, рентген и т.д.)
    - Имеет метаданные: дата, время, описание, AETitle создателя

    ## Иерархия DICOM:
    Patient → **Study** → Series → Instance

    ## Фильтрация
    - По дате исследования (по умолчанию за последний месяц)
    - По модальности-создателю (AETitle)
    - По имени пациента
    """

    query = Study.select(Study, Patient).join(Patient)
    if patient_id:
        query = query.where(Study.patient_id == patient_id)
    else:
        if from_date:
            try:
                from_date_obj = datetime.datetime.strptime(from_date, '%Y-%m-%d').date()
                query = query.where(Study.date >= from_date_obj)
            except ValueError:
                return {"error": "Invalid from date format. Use YYYY-MM-DD"}, 400
        else:
            one_month_ago = datetime.datetime.now().date() - datetime.timedelta(days=30)
            query = query.where(Study.date >= one_month_ago)

        if till_date:
            try:
                till_date_obj = datetime.datetime.strptime(till_date, '%Y-%m-%d').date()
                query = query.where(Study.date <= till_date_obj)
            except ValueError:
                return {"error": "Invalid till date format. Use YYYY-MM-DD"}, 400

        if aetitle:
            aetitles = [aet.strip() for aet in aetitle.split(',')]
            query = query.where(Study.creator_aetitle.in_(aetitles))

        if patient:
            query = query.where(Patient.name ** f'%{patient}%')

    result = []
    for study in query:
        if study.time:
            time_str = study.time[:6]
            hours = time_str[:2]
            minutes = time_str[2:4]
            seconds = time_str[4:6]
            datetime_str = f"{study.date.strftime('%Y-%m-%d')}T{hours}:{minutes}:{seconds}"
        else:
            datetime_str = study.date.strftime('%Y-%m-%d')
        result.append(StudyListResponse(
            uid=study.uid,
            description=study.description,
            date=datetime_str,
            creator_aetitle=study.creator_aetitle,
            patient=study.patient.name if study.patient else None
        ))

    return result

@router.get("/{StudyUID}", summary="Получить детальную информацию об исследовании", response_description="Детальная информация об исследовании")
async def get_study_by_uid(StudyUID: str, db: None = Depends(get_db)) -> StudyDetailResponse:
    """
    Получить детальную информацию об исследовании по StudyUID

    ## Study в DICOM

    Study (исследование) - это совокупность всех диагностических данных,
    полученных в рамках одного медицинского исследования пациента.

    ### Основные характеристики:
    - **Study Instance UID**: Глобально уникальный идентификатор исследования
    - **Study Date/Time**: Дата и время проведения исследования
    - **Study Description**: Описание типа исследования
    - **Modality**: Тип диагностического оборудования (CT, MR, US, etc.)
    - **Accession Number**: Номер направления/заявки

    ### Содержимое исследования:
    - Одна или несколько Series (серий) - наборы связанных изображений
    - Каждая Series содержит Instances (инстансы) - отдельные снимки
    - Может включать Structured Reports (SR) - структурированные отчеты

    ### Использование в PACS:
    Study является основной единицей для поиска, просмотра и управления
    медицинскими изображениями в системе.
    """
    seriesList = [
        SeriesItem(uid=s.uid, description=s.description, modality=s.modality)
        for s in Series.select(Series.uid, Series.description, Series.modality).where(Series.study == StudyUID, Series.modality != 'SR').order_by(Series.number.asc())
    ]
    studyQuery = Study.select(Study, Patient).join(Patient).where(Study.uid == StudyUID)
    if len(studyQuery) == 0:
        raise HTTPException(status_code=404, detail="Study not found")
    study = studyQuery[0]

    res = {
       "study_uid": study.uid,
       "description": study.description,
       "is_stable": True,
       "main_dicom_tags": StudyMainDicomTags(
          institution_name="",
          study_date=study.date.strftime("%Y%m%d"),
          study_id=study.id,
          study_instance_uid=study.uid,
          study_time=study.time
       ),
       "patient_main_dicom_tags": PatientMainDicomTags(
          patient_birth_date=study.patient.birthday.strftime("%Y%m%d") if study.patient.birthday else '',
          patient_id=study.patient.id,
          patient_name=study.patient.name,
          patient_sex=study.patient.sex
       ),
       "series": seriesList,

    }

    return StudyDetailResponse(**res)


async def generate_zip_progress(
    study_uid: str,
    instances,
    study,
    zip_path: str,
    lock_path: str,
    request: Request = None
):
    """
    Генератор SSE событий для создания ZIP архива.

    Args:
        study_uid: UID исследования
        instances: список инстансов исследования
        study: объект исследования
        zip_path: путь для сохранения ZIP файла
        lock_path: путь к lock файлу
    """
    total_instances = len(instances)

    # Последний отправленный процент
    last_sent_percent = -1

    # Начинаем создание нового архива
    yield f"event: start\ndata: {{\"message\": \"Starting archive creation\", \"total\": {total_instances}}}\n\n"
    # Отправляем начальный прогресс 0%
    yield f"event: progress\ndata: {{\"current\": 0, \"total\": {total_instances}, \"percent\": 0}}\n\n"

    try:
        # Проверяем соединение перед началом
        if request and hasattr(request, 'is_disconnected'):
            if await request.is_disconnected():
                raise asyncio.CancelledError("Клиент разорвал соединение")
        # Создаем директорию если нужно
        os.makedirs(os.path.dirname(zip_path), exist_ok=True)

        # Создаем ZIP архив в памяти
        zip_buffer = io.BytesIO()

        with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
            processed = 0
            errors = 0

            for instance in instances:
                try:
                    # Получить DICOM файл
                    study_date = instance.study_date.strftime("%Y%m")
                    dicom_data = getImage(instance.uid, study_date, study.nodeCode, buffer=False)

                    if dicom_data:
                        # Создать имя файла в архиве
                        filename = f"{instance.series.uid}_{instance.number:06d}.dcm"

                        # Сохранить DICOM файл во временный буфер
                        dicom_buffer = io.BytesIO()
                        dicom_data.save_as(dicom_buffer, write_like_original=True)

                        # Добавить файл в ZIP архив
                        zip_file.writestr(filename, dicom_buffer.getvalue())

                        processed += 1

                        # Вычисляем текущий процент
                        progress_percent = int((processed / total_instances) * 100)

                        # Отправляем событие если процент изменился
                        if progress_percent > last_sent_percent or processed == total_instances:
                            last_sent_percent = progress_percent
                            yield f"event: progress\ndata: {{\"current\": {processed}, \"total\": {total_instances}, \"percent\": {progress_percent}}}\n\n"

                except Exception as e:
                    errors += 1
                    logger.error(f"Ошибка при обработке инстанса {instance.uid}: {e}")
                    # Продолжаем несмотря на ошибки

                # Проверяем не разорвано ли соединение
                if request and hasattr(request, 'is_disconnected'):
                    if await request.is_disconnected():
                        raise asyncio.CancelledError("Клиент разорвал соединение")

        # Сохраняем ZIP файл на диск
        zip_buffer.seek(0)
        with open(zip_path, 'wb') as f:
            f.write(zip_buffer.getvalue())

        # Удаляем lock файл
        remove_lock_file(lock_path)

        # Отправляем завершающее событие
        relative_path = zip_path.replace(config.storageDir, "").lstrip("/")
        logger.info(f"ZIP архив для исследования {study_uid} успешно создан: {processed}/{total_instances} файлов, ошибок: {errors}")
        yield f"event: complete\ndata: {{\"url\": \"/images/{relative_path}\", \"processed\": {processed}, \"errors\": {errors}, \"total\": {total_instances}}}\n\n"

    except asyncio.CancelledError as e:
        # Клиент разорвал соединение
        logger.info(f"Создание архива прервано клиентом для исследования {study_uid}")
        remove_lock_file(lock_path)

        # Удаляем частично созданный файл если есть
        try:
            if os.path.exists(zip_path):
                os.remove(zip_path)
        except:
            pass

        # Не отправляем событие error, т.к. клиент уже отключился

    except Exception as e:
        # Ошибка при создании архива
        logger.error(f"Ошибка при создании ZIP архива для исследования {study_uid}: {e}")
        remove_lock_file(lock_path)

        # Удаляем частично созданный файл если есть
        try:
            if os.path.exists(zip_path):
                os.remove(zip_path)
        except:
            pass

        yield f"event: error\ndata: {{\"message\": \"Ошибка при создании архива: {str(e)}\"}}\n\n"


@router.get("/{StudyUID}/zip", summary="Скачать ZIP архив с DICOM файлами исследования", response_description="ZIP архив с DICOM файлами")
async def download_study_zip(
    StudyUID: str,
    db: None = Depends(get_db),
    request: Request = None
) -> Response:
    """
    Скачать ZIP архив со всеми DICOM файлами исследования

    ## Логика работы:
    1. Проверяет существование готового архива
    2. Если архив есть - сразу отдает
    3. Если архива нет - проверяет lock файл
    4. Если lock есть и не просрочен - возвращает SSE поток прогресса
    5. Если lock нет или просрочен - начинает создание архива с SSE

    ## Возвращает:
    - Если архив готов: файл ZIP
    - Если создается: SSE поток с прогрессом
    - Если исследование не найдено: 404
    - Если файлы не найдены: 404
    """
    # Проверить существование исследования
    study_query = Study.select().where(Study.uid == StudyUID)
    if not study_query.exists():
        raise HTTPException(status_code=404, detail="Исследование не найдено")

    study = study_query.get()

    # Получить все инстансы исследования
    instances = (Instance
                .select(Instance, Series)
                .join(Series)
                .where(Series.study == StudyUID)
                .order_by(Series.uid, Instance.number))

    if not instances:
        raise HTTPException(status_code=404, detail="DICOM файлы не найдены для данного исследования")

    # Очистить старые папки с архивами
    cleanup_old_zip_folders()

    # Получить пути
    zip_path = get_zip_file_path(StudyUID)
    lock_path = get_lock_file_path(StudyUID)

    # Проверить, существует ли готовый архив
    if os.path.exists(zip_path):
        # Архив готов, отправляем SSE событие с ссылкой и закрываем соединение
        logger.info(f"ZIP архив для исследования {StudyUID} уже существует, возвращаем ссылку")
        async def send_complete_and_close():
            relative_path = zip_path.replace(config.storageDir, "").lstrip("/")
            yield f"event: complete\ndata: {{\"url\": \"/images/{relative_path}\"}}\n\n"
        
        return StreamingResponse(
            send_complete_and_close(),
            media_type="text/event-stream",
            headers={
                "Cache-Control": "no-cache",
                "Connection": "keep-alive",
                "X-Accel-Buffering": "no"
            }
        )

    # Проверить lock файл
    if os.path.exists(lock_path):
        if is_lock_expired(lock_path):
            # Lock просрочен, удаляем его
            remove_lock_file(lock_path)
        else:
            # Архив уже создается другим процессом, возвращаем ошибку
            raise HTTPException(
                status_code=409,
                detail="Архив уже создается другим процессом. Попробуйте позже."
            )

    # Начать создание нового архива
    # Создаем lock файл
    create_lock_file(lock_path)
    logger.info(f"Начинаем создание ZIP архива для исследования {StudyUID}, файлов: {len(instances)}")

    # Возвращаем SSE поток с созданием архива
    return StreamingResponse(
        generate_zip_progress(StudyUID, instances, study, zip_path, lock_path, request),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no"
        }
    )

@router.delete("/{StudyUID}", summary="Удалить исследование", response_description="Результат удаления")
async def delete_study(StudyUID: str, db: None = Depends(get_db)):
    """
    Удалить исследование из системы PACS.

    ## Процесс удаления:
    1. Проверка прав доступа (только если config.nodeCode == study.nodeCode)
    2. Удаление инстансов из базы данных
    3. Удаление серий из базы данных
    4. Удаление исследования из базы данных
    5. Удаление файлов предпросмотра
    6. Удаление DICOM файлов

    ## Ограничения:
    - Удаление возможно только для исследований созданных на текущем узле
    - Все файлы удаляются физически с диска
    - Операция необратима
    """
    # Проверить существование исследования
    study_query = Study.select().where(Study.uid == StudyUID)
    if not study_query.exists():
        raise HTTPException(status_code=404, detail="Исследование не найдено")

    study = study_query.get()

    # Проверить права доступа
    if config.nodeCode != study.nodeCode:
        raise HTTPException(status_code=403, detail="Удаление разрешено только для исследований созданных на текущем узле")

    # Получить все серии исследования
    series_list = Series.select().where(Series.study == StudyUID)

    # Получить все инстансы исследования
    instances = (Instance
                .select(Instance, Series)
                .join(Series)
                .where(Series.study == StudyUID))

    deleted_files_count = 0
    deleted_previews_count = 0

    # Удалить инстансы и файлы
    for instance in instances:
        try:
            # Удалить DICOM файл
            study_date = instance.study_date.strftime("%Y%m")
            uidhash = hashlib.md5(instance.uid.encode('utf8')).hexdigest()[:2]
            dir_path = os.path.join(config.storageDir, study_date, uidhash)
            file_path = os.path.join(dir_path, instance.uid + '.dcm')

            if os.path.exists(file_path):
                os.remove(file_path)
                deleted_files_count += 1

        except Exception as e:
            # Продолжить удаление других файлов при ошибке
            continue

    # Удалить файлы предпросмотра
    for series in series_list:
        try:
            study_date = study.date.strftime("%Y%m")
            preview_dir = os.path.join(config.storageDir, study_date, 'preview')
            jpg_path = os.path.join(preview_dir, series.uid + '.jpg')

            if os.path.exists(jpg_path):
                os.remove(jpg_path)
                deleted_previews_count += 1

        except Exception as e:
            # Продолжить удаление других превью при ошибке
            continue

    # Удалить записи из базы данных
    try:
        # Удалить инстансы
        Instance.delete().where(Instance.uid.in_([i.uid for i in instances])).execute()

        # Удалить серии
        Series.delete().where(Series.uid.in_([s.uid for s in series_list])).execute()

        # Удалить исследование
        study.delete_instance()

    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Ошибка при удалении из базы данных: {str(e)}")

    return {
        "message": "Исследование успешно удалено",
        "study_uid": StudyUID,
        "deleted_dicom_files": deleted_files_count,
        "deleted_preview_files": deleted_previews_count,
        "deleted_instances": len(instances),
        "deleted_series": len(series_list)
    }
