import io,hashlib
import numpy as np
import pydicom
from pydicom.pixels import apply_voi_lut,apply_modality_lut
from skimage.transform import resize

def finite(ds,name):
    try:
        value=float(getattr(ds,name));return value if np.isfinite(value) else None
    except (AttributeError,ValueError,TypeError):return None

def positive(ds,name):
    v=finite(ds,name);return v if v is not None and v>0 else None

def header(raw):
    # force=True: PACS 캐시의 DICOM 파일은 128바이트 preamble + 'DICM' 매직(File Meta)이 없는 경우가
    # 많다(원시 데이터셋 저장). force 로 그런 파일도 읽는다. MG/단일프레임/MONOCHROME 검증은 아래에서 수행.
    ds=pydicom.dcmread(io.BytesIO(raw),stop_before_pixels=True,force=True)
    if str(getattr(ds,'Modality',''))!='MG': raise ValueError('단일 프레임 MG mammography만 지원합니다.')
    if int(getattr(ds,'NumberOfFrames',1))!=1 or int(getattr(ds,'SamplesPerPixel',1))!=1:
        raise ValueError('DBT 다중 프레임 / 컬러 영상은 지원하지 않습니다.')
    photo=str(getattr(ds,'PhotometricInterpretation',''))
    if photo not in ('MONOCHROME1','MONOCHROME2'):raise ValueError('지원하지 않는 PhotometricInterpretation')
    spacing=None;source=None
    for name in ('PixelSpacing','ImagerPixelSpacing'):
        try:
            values=[float(x) for x in getattr(ds,name)]
            if len(values)==2 and all(np.isfinite(v) and v>0 for v in values):spacing=values;source=name;break
        except (AttributeError,ValueError,TypeError):pass
    view=str(getattr(ds,'ViewPosition','')).upper().strip()
    if not view and hasattr(ds,'ViewCodeSequence'):
        for item in ds.ViewCodeSequence:
            meaning=str(getattr(item,'CodeMeaning','')).lower()
            if 'medio' in meaning and 'oblique' in meaning:view='MLO'
            elif 'cranio' in meaning:view='CC'
    meta={'side':str(getattr(ds,'ImageLaterality',getattr(ds,'Laterality',''))).upper(),
          'view':view,'spacing_mm':spacing,'spacing_source':source,
          'thickness_mm':positive(ds,'BodyPartThickness'),'compression_force_N':positive(ds,'CompressionForce'),
          'kvp':positive(ds,'KVP'),'exposure_mAs':positive(ds,'ExposureInmAs'),
          'intent':str(getattr(ds,'PresentationIntentType','UNKNOWN')),'manufacturer':str(getattr(ds,'Manufacturer','')),
          'photometric':photo,'rows':int(ds.Rows),'columns':int(ds.Columns),
          'bits_stored':int(getattr(ds,'BitsStored',0)),'implant':str(getattr(ds,'BreastImplantPresent','UNKNOWN')),
          'sop_class':str(getattr(ds,'SOPClassUID',''))}
    identity=(str(getattr(ds,'PatientID','')),str(getattr(ds,'IssuerOfPatientID','')),str(getattr(ds,'StudyInstanceUID','')))
    return meta,identity

def normalize(a):
    a=np.asarray(a,dtype=np.float64)
    lo,hi=float(a.min()),float(a.max())
    if not np.isfinite(a).all() or hi-lo<1e-8:raise ValueError('유효한 영상 대비가 없습니다.')
    return ((a-lo)/(hi-lo)).astype('float32')

def read_image(raw,spacing='from_tags',mode='dicom',target_step=.4):
    ds=pydicom.dcmread(io.BytesIO(raw),force=True);meta,_=header(raw)
    if spacing=='from_tags':spacing=meta['spacing_mm']
    a=ds.pixel_array
    if a.ndim!=2:raise ValueError('2D grayscale pixel array가 아닙니다.')
    warnings=[]
    if mode=='legacy':
        a=a.astype('float64'); vendor=meta['manufacturer'].upper()
        if meta['intent']=='FOR PROCESSING':
            a=(16383.-np.log(np.clip(a,1,16383)))**2
            warnings.append('원본 OpenBreast RAW 14-bit 변환 가정 사용')
        elif any(v in vendor for v in ('FUJIFILM','SECTRA','PHILIPS','AGFA')):
            a=a.max()-a
        else:
            a=np.clip(a,0,4095)
            warnings.append('원본 OpenBreast processed 12-bit 범위 가정 사용')
        image=normalize(a)
    else:
        a=apply_modality_lut(a,ds)
        if meta['intent']=='FOR PROCESSING':
            # Dynamic-range-aware logarithmic inversion: adaptation, not MATLAB exact expression.
            a=np.log1p(np.maximum(a.astype('float64')-float(np.min(a)),0))
            a=float(a.max())-a
            warnings.append('FOR PROCESSING: dynamic-range log-inversion 적용; 공급사별 검증 필요')
        else:
            try:
                if np.issubdtype(a.dtype,np.floating) and np.all(a==np.floor(a)):
                    a=a.astype('int64')
                a=apply_voi_lut(a,ds)
            except (ValueError,KeyError,TypeError):warnings.append('VOI LUT 적용 실패: 저장 pixel 사용')
            if meta['photometric']=='MONOCHROME1':a=float(np.max(a))+float(np.min(a))-a
        image=normalize(a)
    if spacing:
        dy,dx=spacing
        step=max(float(target_step),image.shape[0]*dy/1600,image.shape[1]*dx/1600)
        # Rounded working dimensions yield slightly different actual row/column spacing.
        shape=(max(16,int(round(image.shape[0]*dy/step))),max(16,int(round(image.shape[1]*dx/step))))
        working_spacing=[image.shape[0]*dy/shape[0],image.shape[1]*dx/shape[1]]
        # Record actual anisotropy; MAG uses mean step, geometry uses actual values.
        mag_step=float(np.mean(working_spacing))
    else:
        scale=min(1,1200/max(image.shape));shape=(max(16,int(round(image.shape[0]*scale))),max(16,int(round(image.shape[1]*scale))))
        working_spacing=None;mag_step=None
        warnings.append('pixel spacing 미확인: 물리 크기·밀도·체적 미측정')
    image=resize(image,shape,order=1,preserve_range=True,anti_aliasing=True).astype('float32')
    return image,working_spacing,mag_step,warnings

def checksum(raw):return hashlib.sha256(raw).hexdigest()
