import asyncio
import base64
import hashlib
import io
import json
import mimetypes
import os
import os.path
import shutil
import sys
import time
import urllib.request
import uuid
from io import BytesIO
from urllib.parse import urlparse
from urllib.request import Request, urlopen

import aiofiles
import aiohttp
import cloudinary
import cloudinary.uploader
import cv2
import ffmpeg
import numpy as np
import pydash
import pytesseract
import requests
from adafri.v1.files import File
from bs4 import BeautifulSoup
from cv2.gapi import cv
from google.cloud import storage
from PIL import Image
from PIL.Image import Image as PILImage
from werkzeug.utils import secure_filename

# def get_driver_options():
#     options = webdriver.ChromeOptions()
#     options.add_argument("--headless")
#     options.add_argument("--disable-gpu")
#     options.add_argument("window-size=1024,768")
#     options.add_argument("--no-sandbox")
#     options.add_argument('--ignore-certificate-errors')
#     options.add_argument("--test-type")
#     return options;

def get_storage_client():
    storage_credentials_path = os.environ.get('CLOUD_STORAGE_CREDENTIALS_PATH', None)
    storage_client = storage.Client.from_service_account_json(storage_credentials_path)
    return storage_client
def get_html_content(url, props, wait=1):
    # print('url', url)
    # print('url', props)
    #browser = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=get_driver_options());
    # browser.get(url);
    # browser.implicitly_wait(wait);
    # source = browser.page_source;
    # browser.close();
    source = urllib.request.urlopen(url);
    soup = BeautifulSoup(source, "html.parser");
    results = {};
    props_results = {};
    prop_key = 'key';
    prop_value = 'value';
    attr_key = 'attr';
    for prop in props:
        attrs = {};
        attrs[prop[attr_key]] = prop[prop_key]
        # print('find for', prop)
        # print('find for attr', prop[attr_key])
        props_results[prop[prop_key]]  = soup.find_all(attrs=attrs);
    for prop in props:
        if prop[prop_key] in props_results:
            for p in props_results[prop[prop_key]]:
                pv = prop[prop_value]
                # print(f"prop {prop[prop_key]} {pv}  result {p}");
                if p.has_attr(pv):
                    results[prop[prop_key]] = p[pv] 
    return results;

async def get_content_length(url):
    async with aiohttp.ClientSession() as session:
        async with session.head(url) as request:
            return request.content_length


def parts_generator(size, start=0, part_size=10 * 1024 ** 2):
    while size - start > part_size:
        yield start, start + part_size
        start += part_size
    yield start, size


async def download(url, headers, save_path):
    print(save_path)
    async with aiohttp.ClientSession(headers=headers) as session:
        async with session.get(url) as request:
            file = await aiofiles.open(save_path, 'wb')
            buffer = await request.content.read();
            await file.write(buffer)

async def downloadFile(file_input, save_path):
    # print(save_path)
    file = await aiofiles.open(save_path, 'wb')
    await file.write(file_input.read())


async def process(url):
    filename = os.path.basename(urlparse(url).path)
    #tmp_dir = TemporaryDirectory(prefix=filename, dir=f"{os.path.abspath('.')}/uploads/");
    size = await get_content_length(url)
    tasks = []
    file_parts = []

    for number, sizes in enumerate(parts_generator(size)):
        part_file_name = os.path.join(os.getcwd(), 'uploads/'+secure_filename(f'{filename}.part{number}'));
        file_parts.append(part_file_name)
        tasks.append(download(url, {'Range': f'bytes={sizes[0]}-{sizes[1]}'}, part_file_name))
    
    await asyncio.gather(*tasks)
    with open(os.path.join(os.getcwd(), 'uploads/'+filename), 'wb') as wfd:
        for f in file_parts:
            with open(f, 'rb') as fd:
                shutil.copyfileobj(fd, wfd)
                os.remove(f)

async def processFile(file):
    filename = file.filename
    #tmp_dir = TemporaryDirectory(prefix=filename, dir=f"{os.path.abspath('.')}/uploads/");
    size = len(file.read())
    tasks = []
    file_parts = []

    for number, sizes in enumerate(parts_generator(size)):
        part_file_name = os.path.join(os.getcwd(), 'uploads/'+secure_filename(f'{filename}.part{number}'));
        file_parts.append(part_file_name)
        tasks.append(downloadFile(file, part_file_name))
    
    await asyncio.gather(*tasks)
    
    with open(filename, 'wb') as wfd:
        for f in file_parts:
            with open(f, 'rb') as fd:
                shutil.copyfileobj(fd, wfd)
                os.remove(f)

async def main(url):
    await asyncio.gather(*[process(url)])

async def main_file(file):
    await asyncio.gather(*[processFile(file)])

def get_prob_attribute(url):
    hash = get_video_hash(url);
    print('video hash', hash);
    filename = os.path.basename(urlparse(url).path);
    directory_path = f"{os.path.abspath('.')}/uploads"
    abs_path = f"{directory_path}/{filename}"
    pre, ext = os.path.splitext(f"{directory_path}/{filename}")
    abs_path_without_ext = f"{pre}"    
    mime_type = "";
    extension = "";
    mpeg = ['mpeg','mpegts','mpegvideo']
    prob = ffmpeg.probe(abs_path)
    if prob['format']['format_name']=='mov,mp4,m4a,3gp,3g2,mj2':
        mime_type="video/mp4";
        extension = "mp4";
    elif prob['format']['format_name'] in mpeg:
        mime_type="video/mpeg";
        extension = prob['format']['format_name'];
    prob['type']="VIDEO"
    #print('prob', prob)

    prob['content_type'] = mime_type;
    prob['abs_path'] = abs_path;
    prob['abs_path_without_ext'] = abs_path_without_ext;
    prob['extension'] = extension;
    prob['filename'] = filename
    prob['directory_path'] = directory_path

    video_stream = next((stream for stream in prob['streams'] if stream['codec_type'] == 'video'), None)
    audio_stream = next((stream for stream in prob['streams'] if stream['codec_type'] != 'video'), None)
    if video_stream is not None:
        video_stream_attributes = ['avg_frame_rate', 'width', 'height', 'coded_width', 'coded_height', 'bit_rate', 'display_aspect_ratio', 'r_frame_rate', 'nb_frames', 'r_frame_rate', 'sample_aspect_ratio']
        video_prop = pydash.pick(video_stream, video_stream_attributes);
        video_prop['video_biterate'] = video_prop['bit_rate']
        del video_prop['bit_rate']
        prob = {**prob, **video_prop};

    del prob['format']['filename']
    prob['_hash'] = hash;
    return prob; 

def resize(image):
    maxsize = (1920, 1080)
    image.thumbnail(maxsize)


def resize_image_keep_ratio(image_path: str, target_width: int = None, target_height: int = None, 
    output_path: str = None, quality: int = 95) -> dict:
    """
    Resize image while maintaining aspect ratio.
    
    Args:
        image_path: Path to input image
        target_width: Target width (if None, height will be used)
        target_height: Target height (if None, width will be used)
        output_path: Output path (if None, overwrites original)
        quality: JPEG quality (1-100)
    
    Returns:
        Dict with original and new dimensions, file sizes
    """
    try:
        # Open image
        with Image.open(image_path) as img:
            # Get original dimensions
            original_width, original_height = img.size
            original_size = os.path.getsize(image_path)
            
            # Calculate new dimensions maintaining aspect ratio
            if target_width and target_height:
                # Both dimensions specified - fit within bounds
                ratio = min(target_width / original_width, target_height / original_height)
                new_width = int(original_width * ratio)
                new_height = int(original_height * ratio)
            elif target_width:
                # Width specified - calculate height
                ratio = target_width / original_width
                new_width = target_width
                new_height = int(original_height * ratio)
            elif target_height:
                # Height specified - calculate width
                ratio = target_height / original_height
                new_width = int(original_width * ratio)
                new_height = target_height
            else:
                return {"error": "Must specify either width or height"}
            
            # Resize image
            resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
            
            # Determine output path
            if not output_path:
                output_path = image_path
            
            # Save resized image
            if output_path.lower().endswith('.jpg') or output_path.lower().endswith('.jpeg'):
                resized_img.save(output_path, 'JPEG', quality=quality, optimize=True)
            elif output_path.lower().endswith('.png'):
                resized_img.save(output_path, 'PNG', optimize=True)
            else:
                resized_img.save(output_path)
            
            # Get new file size
            new_size = os.path.getsize(output_path)
            
            return {
                "success": True,
                "original_dimensions": (original_width, original_height),
                "new_dimensions": (new_width, new_height),
                "original_size_kb": original_size // 1024,
                "new_size_kb": new_size // 1024,
                "size_reduction_percent": round((1 - new_size / original_size) * 100, 2),
                "output_path": output_path
            }
            
    except Exception as e:
        return {"error": f"Resize failed: {str(e)}"}

def resize_image_to_max_dimensions(image_path: str, max_width: int, max_height: int, 
    output_path: str = None, quality: int = 95) -> dict:
    """
    Resize image to fit within maximum dimensions while keeping aspect ratio.
    Only resizes if image exceeds the maximum dimensions.
    """
    try:
        with Image.open(image_path) as img:
            original_width, original_height = img.size
            
            # Check if resizing is needed
            if original_width <= max_width and original_height <= max_height:
                return {
                    "success": True,
                    "resized": False,
                    "message": "Image already within bounds",
                    "dimensions": (original_width, original_height)
                }
            
            # Calculate new dimensions
            ratio = min(max_width / original_width, max_height / original_height)
            new_width = int(original_width * ratio)
            new_height = int(original_height * ratio)
            
            # Resize
            resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
            
            # Save
            if not output_path:
                output_path = image_path
                
            if output_path.lower().endswith('.jpg') or output_path.lower().endswith('.jpeg'):
                resized_img.save(output_path, 'JPEG', quality=quality, optimize=True)
            else:
                resized_img.save(output_path)
            
            return {
                "success": True,
                "resized": True,
                "original_dimensions": (original_width, original_height),
                "new_dimensions": (new_width, new_height),
                "output_path": output_path
            }
            
    except Exception as e:
        return {"error": f"Resize failed: {str(e)}"}

def replace_filename(_url: str, new_filename: str, _extension: str | None = None) -> str:
    last = _url.split('/')[-1]
    print('base', last)
    extension = last.split('.')[-1]
    if extension is None:
        extension = _extension
    file_name = last.split('/')[-1].split('.')[0]
    return _url.replace(last, f"{new_filename}_{file_name}.{extension}")


def get_image_attribute(url, max_width=1200, max_height=1200):
    im = Image.open(url);
    prob = {};
    width, height = im.size;
    # if width > max_width or height > max_height:
    #     print('resizing image', url)
    #     resized_url = replace_filename(url, "resized")
    #     resized = resize_image_to_max_dimensions(
    #         url, 
    #         max_width=max_width, 
    #         max_height=max_height, 
    #         output_path=resized_url
    #     )
    #     if resized  is not None and 'new_dimensions' in resized:
    #         width, height = resized['new_dimensions']
    #         print('new im after resize', width, height)
    #         im = Image.open(resized_url);
    #         im.save(url)
    #         os.remove(resized_url)

    filename = os.path.basename(urlparse(url).path);
    directory_path = f"{os.path.abspath('.')}/uploads"
    abs_path = f"{directory_path}/{filename}"
    pre, ext = os.path.splitext(f"{directory_path}/{filename}")
    abs_path_without_ext = f"{pre}"  
    size = os.stat(abs_path).st_size
    format = im.format;
    mime_type = str(format).lower();
    extension = str(format).lower();
    prob['width'] = width;
    prob['height'] = height
    prob['extension'] = extension
    prob['mime_type'] = mime_type
    jpg = ['jpeg', 'jpg']
    png = ["png"]
    gif = ["gif"]
    prob['mode'] = im.mode
    hash = None;
    if 'gif' in mime_type:
        hash = get_image_hash_pillow(url);
    else:
        hash = get_image_hash(url);
    if str(mime_type).lower() in jpg:
        prob['content_type'] = 'image/jpeg';
    elif mime_type in png:
        prob['content_type'] = 'image/jpeg';
    elif mime_type in gif:
        prob['content_type'] = 'image/gif';    #print('prob', prob)
    prob['content_type'] = mime_type;
    prob['abs_path'] = abs_path;
    prob['abs_path_without_ext'] = abs_path_without_ext;
    prob['extension'] = extension;
    prob['filename'] = filename
    prob['directory_path'] = directory_path
    prob['size'] = size
    prob['_hash'] = hash;
    return prob; 

def resumable_upload(url, remove=True):
    start_code = time.monotonic();
    asyncio.new_event_loop().run_until_complete(main(url))
    print(f'{time.monotonic() - start_code} seconds!')
    prob = get_prob_attribute(url);
    if remove is True:
        os.remove(prob['abs_path']);
    return prob;

def resumable_file_upload(file, extensions, upload_path, remove=True):
    name = file.filename;
    print('filename', file.filename)
    if file.filename == 'blob':
        ext = ".png";
        if file.content_type == 'image/jpeg':
            ext = ".jpg"
        name = name + ext
    filename = secure_filename(name)
    print('secure filename', secure_filename);
    path = os.getcwd() + "/"+ os.path.join(upload_path, filename);
    print(path)
    if filename != '':
        file_ext = os.path.splitext(filename)[1]
        if file_ext not in extensions:
            return None
        file.save(path)
    save_path = path

    prob = get_prob_attribute(path);
    if remove is True:
        os.remove(save_path);
    return prob;

def resumable_image_upload(file, extensions, fname=None, upload_path=None, remove=True):
    # name = None
    name = file.filename;
    if name == 'blob':
        name = f"file~{uuid.uuid1()}"
        ext = ".png";
        if file.content_type == 'image/jpeg':
            ext = ".jpg"
        name = name + ext

    if fname is not None and bool(fname):
        name = fname  

    filename = secure_filename(name);
    path = None
    if os.path.isabs(upload_path):
        path = os.path.join(upload_path, filename);
    else:
        path = os.path.join(os.getcwd(), upload_path, filename);
    # print('path', path)
    if filename != '':
        file_ext = getattr(file, 'extension', None)
        if file_ext is None:
            file_ext = os.path.splitext(filename)[1]
        # print('ext attr', ext)
        if file_ext not in extensions:
            print('file ext not in extensions', extensions)
            return None
        custom = getattr(file, 'is_custom', False)
        print('custom', custom)
        if getattr(file, 'is_custom', False) is True:
            file.image.save(path)
        else:
            file.save(path)
    save_path = path
    print('file saved on', save_path)

    prob = get_image_attribute(path);
    
    if remove is True:
        os.remove(save_path);
    return prob;


def generate_thumbnail(in_filename, out_filename):
    probe = ffmpeg.probe(in_filename)
    time = float(probe['streams'][0]['duration']) // 2
    width = probe['streams'][0]['width']
    try:
        (
            ffmpeg
            .input(in_filename, ss=time)
            .filter('scale', width, -1)
            .output(out_filename, vframes=1)
            .overwrite_output()
            .run(capture_stdout=True, capture_stderr=True)
        )
    except ffmpeg.Error as e:
        print(e.stderr.decode(), file=sys.stderr)
        sys.exit(1)


def crop_video(url, crop, save_key=uuid.uuid1()):
    ratio = 1
    print("crop", crop)
    if 'crop_based_width' in crop and crop['crop_based_width'] is not None:
        capture = cv2.VideoCapture(url)
        video_width = int(capture.get(cv.CAP_PROP_FRAME_WIDTH));
        ratio = crop['crop_based_width']/video_width;
        capture.release();
        
    x = int(crop['x']/ratio);
    y = int(crop['y']/ratio); 
    xPosition = int(crop['xPosition']/ratio);
    yPosition = int(crop['yPosition']/ratio);
    crop_format = {"x": x, "y": y, "xPosition": xPosition, "yPosition": yPosition};
    crop_hash = f"{hashlib.sha1(str(str(json.dumps(crop_format)) + '~' + save_key).encode()).hexdigest()}"
    prob = resumable_upload(url, False);
    #print(prob)
    outputName = f"{prob['abs_path_without_ext']}~{crop_hash}.{prob['extension']}";
    thumbnailName = f"{prob['abs_path_without_ext']}~{crop_hash}.jpg";
    #print('output', outputName)
    crop = ffmpeg.input(prob['abs_path']).filter('crop', xPosition, yPosition, x, y).output(outputName).run();
    crop_prob = get_prob_attribute(outputName);
    thumbnail = generate_thumbnail(outputName, thumbnailName)
    print('crop prob', crop_prob)
    crop_prob['parent_path'] = prob['abs_path']
    crop_prob['thumbnail'] = thumbnailName
    #os.remove(prob['abs_path'])
    return crop_prob;

def crop_image_fn(url, crop):
    crop_hash = hashlib.sha1(json.dumps(crop).encode()).hexdigest();
    im = Image.open(requests.get(url, stream=True).raw);
    format = im.format;
    extension = str(format).lower();
    mime_type = str(format).lower();
    content_type = ""
    jpg = ['jpeg', 'jpg']
    png = ["png"]
    gif = ["gif"]
    if str(mime_type).lower() in jpg:
        content_type = 'image/jpeg';
    elif mime_type in png:
        content_type = 'image/png';
    elif mime_type in gif:
        content_type = 'image/gif';
    filename_hash = hashlib.sha1(str(f"crop~{os.path.basename(urlparse(url).path)}~{crop_hash}").encode()).hexdigest();
    filename = f"{filename_hash}.{extension}" 
    directory_path = f"{os.path.abspath('.')}/uploads"
    abs_path = f"{directory_path}/{filename}"
    pre, ext = os.path.splitext(f"{directory_path}/{filename}")
    abs_path_without_ext = f"{pre}"
    image_width, image_height = im.size;
    if str(ext).lower() == 'png':
        extension = "PNG"  
    crop_based_width = image_width;
    if 'crop_based_width' in crop and crop['crop_based_width'] is not None:
        crop_based_width = crop['crop_based_width']
    ratio = crop_based_width/image_width;
    # print('ratio',  ratio)
    x = int(crop['x']/ratio);
    y = int(crop['y']/ratio); 
    xPosition = int(crop['xPosition']/ratio);
    yPosition = int(crop['yPosition']/ratio);

    im.crop((x, y, xPosition, yPosition)).save(abs_path, extension, optimize=True);
    image_cropped = Image.open(abs_path)
    st_size = os.stat(abs_path).st_size
    prob = {
        'width': image_cropped.width,
        'height': image_cropped.height,
        'extension': extension,
        'size': st_size,
        'mime_type': mime_type,
        'type': "IMAGE",
        'content_type': content_type,
        'abs_path': f"{abs_path_without_ext}.{extension}",
        'abs_path_without_ext': abs_path_without_ext,
        'filename': filename
    }
    return prob;


def hash_array_to_hash_hex(hash_array):
  # convert hash array of 0 or 1 to hash string in hex
  hash_array = np.array(hash_array, dtype = np.uint8)
  hash_str = ''.join(str(i) for i in 1 * hash_array.flatten())
  hash_hex = (hex(int(hash_str, 2)));
  hash = hashlib.sha1(str(hash_hex).encode()).hexdigest();
  return hash

def hash_hex_to_hash_array(hash_hex):
  # convert hash string in hex to hash values of 0 or 1
  hash_str = int(hash_hex, 16)
  array_str = bin(hash_str)[2:]
  arr = np.array([i for i in array_str], dtype = np.float32)
  string = np.array2string(arr);
  hash = hashlib.sha1(str(hash).encode()).hexdigest();
  print('hash', hash);
  return hash;


def get_image_hash(image):
    img = cv2.imread(image);
  # resize image and convert to gray scale
    img = cv2.resize(img, (64, 64))
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    img = np.array(img, dtype = np.float32)
    # calculate dct of image 
    dct = cv2.dct(img)
    # to reduce hash length take only 8*8 top-left block 
    # as this block has more information than the rest
    dct_block = dct[: 8, : 8]
    # caclulate mean of dct block excluding first term i.e, dct(0, 0)
    dct_average = (dct_block.mean() * dct_block.size - dct_block[0, 0]) / (dct_block.size - 1)
    # convert dct block to binary values based on dct_average
    dct_block[dct_block < dct_average] = 0.0
    dct_block[dct_block != 0] = 1.0
    # store hash value
    hash = hash_array_to_hash_hex(dct_block.flatten());
    print('hash', hash);
    return hash;
def get_image_hash_pillow(image):
    img = Image.open(image)
    img = img.resize((10, 10), Image.ANTIALIAS)
    img = img.convert("L")
    pixel_data = list(img.getdata())
    avg_pixel = sum(pixel_data)/len(pixel_data)
    bits = "".join(['1' if (px >= avg_pixel) else '0' for px in pixel_data])
    hex_representation = str(hex(int(bits, 2)))[2:][::-1].upper();
    hash = hashlib.sha1(str(hex_representation).encode()).hexdigest();
    print('hash', hash);
    return hash;
def get_video_hash(video):
    print(video)
    cap = cv2.VideoCapture(video)
    frames = [];
    hashes = [];
    check = True
    i = 0
    bgst = cv2.createBackgroundSubtractorMOG2()


    while check:
        check, frame = cap.read()
        # if not i % 20: 
    # When everything done, release the capture
        # np_arr = np.array(frames);
        #img = cv2.resize(frame, (64, 64))
        if frame is not None:
            img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            img = np.array(img, dtype = np.float32)
            dct = cv2.dct(img)
            # to reduce hash length take only 8*8 top-left block 
            # as this block has more information than the rest
            dct_block = dct[: 8, : 8]
            # caclulate mean of dct block excluding first term i.e, dct(0, 0)
            dct_average = (dct_block.mean() * dct_block.size - dct_block[0, 0]) / (dct_block.size - 1)
            # convert dct block to binary values based on dct_average
            dct_block[dct_block < dct_average] = 0.0
            dct_block[dct_block != 0] = 1.0
            # store hash value
            hash = hash_array_to_hash_hex(dct_block.flatten());
            #print('frame hash', hash)
            hashes.append(hash)
        # print('check', check) # This line is if you want to subsample your video
        # print('arr', arr)         # (i.e. keep one frame every 20)
        # frames.append(arr)
        #i += 1
    cap.release()

    hash_data = hashlib.sha1(str(''.join(hashes)).encode()).hexdigest();
    #print('hash', hash_data);
    return hash_data;


def text_detection(input):
    image = None
    try:
        if isinstance(input, str) is True:
            image = Image.open(input);
        elif input is not None:
            image = input;
        text = pytesseract.image_to_string(image);
        #print the text line by line
        print(text[:-1])
        print(text)
        return text[:-1].splitlines();
    except Exception as e:
        return None;

def analyze(urls=[]):
    try:
        url = f"{ANALYZE_API_URL}/analyzeImage"
        data_object = {'images_url': urls}
        x = requests.post(url, json = data_object)
        data = x.json()
        if len(data)==0:
            return None;
        return data[0]['colors'];
    except Exception as e:
        return None;

def get_content_type(_format: str):
    format = _format.lower()
    if format == 'jpeg':
        format = 'jpg'
    return f"image/{format}"

def upload_blob_from_blob_From_File(file_source_name, bucket_name,  destination_blob_name, project_id):
    #os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "adafri-backend-google-b205ae995e46.json"
    storage_client = get_storage_client()
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(destination_blob_name)
    # local_size_kb = os.stat(file_source_name).st_size // 1024
    #print(bucket.cors)
    image = Image.open(file_source_name)
    # mime_type = image.get_format_mimetype()
    # print('mime_type', mime_type)
    # st_size = os.stat(file_source_name).st_size
    # print('stat size', st_size)
    cropped_bytes = image.tobytes()
    # local_size_kb = len(cropped_bytes) // 1024
    # blob.upload_from_string(
    #         cropped_bytes,
    #         content_type=mime_type,
    #         timeout=300
    # )
    use_cloudinary = as_bool(os.environ.get('USE_CLOUDINARY', False))
    if use_cloudinary:
        up = cloudinary_upload(file_source_name, bucket_name, destination_blob_name)
        if up is None:
            return None
        return up
    blob.upload_from_filename(file_source_name, timeout=300)
    
    blob.make_public();
    # blob.reload()
    gcs_size_kb = blob.size
    # print('local_size_kb', local_size_kb)
    # print('blob.size', gcs_size_kb // 1024)
    return {"file_url": blob.public_url, "bucket_name": bucket_name, "size": gcs_size_kb, "blob_name": blob.name, "created_time": blob.time_created.strftime("%Y%m%d"), "blob_id": blob.id, "content_type": blob.content_type}


def cloudinary_upload(filename, bucket_name='adafri_upload', blob_name='image_upload'):
    try:
        print('cloudinary upload', filename)
        up = cloudinary.uploader.upload(filename)
        if up is None:
            return None
        up['file_url'] = up['secure_url']
        up['bucket_name'] = bucket_name
        up['blob_name'] = blob_name
        up['content_type'] = get_content_type(up['format'])
        up['size'] = up['bytes']
        print('cloudinary upload response', up)
        return up
    except Exception as e:
        print('cludinary exception upload', e)
        return None
def as_bool(val):
    if isinstance(val, bool):
        return val
    return val.lower() in ('true', '1')


class FileWrapper(PILImage):
    image: PILImage
    is_custom = True
    extension = ''
    content_type = ''
    def __init__(self, file_path=None, filename=None, image_data=None):
        super().__init__()
        self.file_path = file_path
        self.filename = filename
        self.image_data = image_data
        self.is_custom = True
        if image_data is not None:
            self.file_size = len(image_data)
    
    def get_format(self, value: str):
        if value.lower() == 'jpeg':
            return '.jpg'
        return f'.{value}'
    def open(self):
        """Open and validate the image data"""
        try:
            # Create image from bytes without verification (which can be destructive)
            image = Image.open(io.BytesIO(self.image_data))
            
            # Get image format and set extension
            im_format = image.format
            if im_format:
                file_extension = self.get_format(im_format)
                self.extension = file_extension
                print('file_extension', file_extension)
                
                # Set content type
                if im_format.upper() == 'PNG':
                    self.content_type = 'image/png'
                elif im_format.upper() in ['JPEG', 'JPG']:
                    self.content_type = 'image/jpeg'
                elif im_format.upper() == 'GIF':
                    self.content_type = 'image/gif'
                else:
                    self.content_type = f'image/{im_format.lower()}'
            else:
                # Default to jpg if format can't be determined
                self.extension = '.jpg'
                self.content_type = 'image/jpeg'
            
            self.image = image
            return self
            
        except Exception as e:
            print('Exception while openeing', e)
            return None
            # raise ValueError(f"Failed to open image: {e}")
    
    def save(self, path):
        """Save the image data to a file"""
        try:
            with open(path, 'wb') as f:
                f.write(self.image_data)
        except Exception as e:
            raise ValueError(f"Failed to save image: {e}")
    
    # def read(self):
    #     with open(self.file_path, 'rb') as f:
    #         return f.read()


def upload_image_from_string(image_string, extensions, upload_path, accountId: str, public=True, process=True, name=None, bucket_name='adafri_upload', upload_folder='image_upload', project_id="adafri-backend") -> 'File | None':
    """
    Upload image from a data URL string (e.g., "data:image/png;base64,iVBORw0KGgo...")
    
    Args:
        image_string: Data URL string starting with "data:image/..."
        extensions: Allowed file extensions
        upload_path: Path to save the image
        accountId: User account ID
        public: Whether to make the file public
        process: Whether to process the image
        name: Custom filename
        bucket_name: Cloud storage bucket name
        upload_folder: Folder within the bucket
        project_id: Google Cloud project ID
    
    Returns:
        File object or None if upload fails
    """
    try:
        # Parse the data URL
        if not image_string.startswith('data:image/'):
            raise ValueError("Invalid data URL format. Must start with 'data:image/'")
        
        # Extract MIME type and base64 data
        header, encoded_data = image_string.split(',', 1)
        mime_type = header.split(':')[1].split(';')[0]
        
        # Get file extension from MIME type
        extension = mimetypes.guess_extension(mime_type)
        if not extension:
            extension = '.png'  # Default to PNG if can't determine
        
        # Remove the dot from extension
        extension = extension.lstrip('.')
        
        # Validate extension
        if f".{extension.lower()}" not in [ext.lower() for ext in extensions]:
            raise ValueError(f"File extension '{extension}' not allowed. Allowed: {extensions}")
        
        # Decode base64 data
        try:
            image_data = base64.b64decode(encoded_data)
        except Exception as e:
            raise ValueError(f"Invalid base64 data: {str(e)}")
        
        # Validate image data
        
        # Generate filename if not provided
        if not name:
            timestamp = int(time.time())
            name = f"image_{timestamp}_{hashlib.md5(image_data).hexdigest()[:8]}"
        
        # Create temporary file path
        temp_filename = f"{name}.{extension}"
        name = temp_filename
        temp_file_path = os.path.join(upload_path, temp_filename)
        image = None
        try:
            file_wrapper = FileWrapper(temp_file_path, temp_filename, image_data)
            image = file_wrapper.open()
            width, height = image.size
        except Exception as e:
            raise ValueError(f"Invalid image data: {str(e)}")
        
        # Ensure upload directory exists
        os.makedirs(upload_path, exist_ok=True)
        
        # Save image to temporary file
        with open(temp_file_path, 'wb') as f:
            f.write(image_data)
        
        # Create a file-like object for the upload function
        
        # Use existing upload function
        result = upload_image_from_file(
            file=image,
            extensions=extensions,
            upload_path=upload_path,
            accountId=accountId,
            public=public,
            process=process,
            name=name,
            bucket_name=bucket_name,
            upload_folder=upload_folder,
            project_id=project_id
        )
        
        # Clean up temporary file
        try:
            os.remove(temp_file_path)
        except Exception as e:
            print(f"Warning: Could not remove temporary file {temp_file_path}: {e}")
        
        return result
        
    except Exception as e:
        print(f"Error uploading image from string: {str(e)}")
        return None
def upload_image_from_file(file, extensions, upload_path, accountId: str, public=True, process=True, name=None, bucket_name='adafri_upload', upload_folder='image_upload', project_id="adafri-backend") ->'File | None':
    # Use the bucket to get a blob object;
    #upload = upload_file(file, accountId, False, True, bucket_name)
    resumable = resumable_image_upload(file=file, extensions=extensions, fname=name, upload_path=upload_path, remove=False);
    if resumable is None:
        return None
    
    filenameHash = f"{hashlib.sha1(str(resumable['abs_path_without_ext'] + '~' + accountId).encode()).hexdigest()}";
    filename = f"{filenameHash}.{resumable['extension']}"
    blob_name = f"{upload_folder}/{accountId}/{filename}";
    obj_str = blob_name + '~'+ str(resumable['_hash']) + '~' + accountId;
    use_cloudinary = as_bool(os.environ.get('USE_CLOUDINARY', False))
    if 'size' in resumable:
        obj_str = obj_str + '~' + str(resumable['size'])
    if 'width' in resumable:
        print('width resumable', resumable['width'])
        obj_str = obj_str + '~' + str(resumable['width'])
    if 'height' in resumable:
        obj_str = obj_str + '~' + str(resumable['height'])
    if use_cloudinary:
        obj_str = obj_str + '~' + os.getenv('CLOUDINARY_API_KEY','cloudinary')


    # obj_id = f"{hashlib.sha1(str(str(resumable['_hash']) + '~' + accountId).encode()).hexdigest()}";
    # print('resumable', resumable)
    obj_id = f"{hashlib.sha1(str(obj_str).encode()).hexdigest()}"
    file = File(file=obj_id, collection_name=f"{upload_folder}").get();
    if file is not None:
        return file
    print('uploading start')
    size = 0
    url = ""
    blob_bucket_name = bucket_name
    blob_id=None
    type=None
    created_time=None
    collection = upload_folder
    file_path = f"{resumable['directory_path']}/{resumable['filename']}"
    
    print('use_cloudinary', use_cloudinary)
    if use_cloudinary:
        print('upload_image_from_file with cloudinary', file_path)
        up = cloudinary_upload(file_path, bucket_name, blob_name)
        if up is None:
            print("cloudinary exception upload", up)
            return None
        up['id'] = obj_id
        up['_id'] = obj_id
        up['obj_id'] = obj_id
        type = up['content_type']
        size = up['size']
        url = up['file_url']
    else:
        print('upload_image_from_file with gcloud', file_path)
        storage_client = get_storage_client()
        bucket = storage_client.bucket(bucket_name)
        blob = bucket.blob(blob_name)
        try:
            os_file_path = file_path
            #os.path.join(os.getcwd(), 'uploads/'+resumable['filename']);
            blob.upload_from_filename(filename=os_file_path)
        except Exception as e:
            print("Exception catched", e);
            return {"error": e}
        # Make the object public
        if public:
            blob.make_public()
        url = blob.public_url
        size = blob.size
        blob_bucket_name = blob.bucket.name
        type = blob.content_type
        blob_name = blob.name
        blob_id = blob.id
        created_time = blob.time_created
   
    analyzed = None;
    text_detected = None;
    if process is True:
        try:
            analyzed = analyze([url]);
            text_detected = text_detection(file_path);
        except Exception as e:
            print("Exception catched", e);
    try:
        os.remove(file_path);
    except Exception as e:
            print("Exception catched", e);
    #blob_split  = blob.id.split('/')
    #blob_id = blob_split[len(blob_split)-1]; 
    video_obj = {"file_source": None, "file_url": url, "bucket_name": blob_bucket_name, "accountId": accountId, "size": size, "blob_name": blob_name, "created_time": created_time, "blob_id": blob_id, "type": type, **resumable}
    video_info = pydash.omit(video_obj, ['abs_path', 'abs_path_without_ext', 'parent_path'])
    if analyzed is not None and len(analyzed)>0:
        video_info['colors'] = analyzed;
    if text_detected is not None and len(text_detected)>0:
        video_info['text'] = text_detected;
    #obj_id = f"{hashlib.sha1(str(blob.name + '~' + str(blob.size) + '~' + accountId).encode()).hexdigest()}"
    video_info['_id'] = obj_id
    video_info['id'] = obj_id
    # video_info['testMode'] = True 
    # print('video obj', video_info)
    file_create = File(collection_name=f"{upload_folder}").create(**video_info);
    # print('file_create', file_create)
    if file_create is None or file_create.status.lower()!="ok":
        return None
    # db = FirebaseCollectionBase(collection_name=collection).db
    # db.collection(collection).document(obj_id).set(video_info, merge=True)
    # return video_info
    return file_create.data
def upload_image_from_url(file, extensions, upload_path, accountId: str, public=True, process=True, name=None, bucket_name='adafri_upload', upload_folder='image_upload', project_id="adafri-backend") ->'File':
    # Use the bucket to get a blob object;
    #upload = upload_file(file, accountId, False, True, bucket_name)
    image = None;
    try:
        # os.makedirs(f"{upload_path}/{accountId}", exist_ok=True)
        image = get_image_bytes(file)
    except Exception as e:
        print("Exception catched", e);
    collection = upload_folder
    # print('image', image)
    if image is None:
        return None
        
    storage_client = get_storage_client()
    bucket = storage_client.bucket(bucket_name)
    image_bytes, pic = image
    im_format = pic.format
    width, height = pic.size
    if name is None:
        name = f"image-{uuid.uuid1()}"
    file_extension = '';
    if f".{im_format.lower()}" in  extensions:
        file_extension = "." + im_format.lower()
    filename = f"{name}{file_extension}"
    blob_name = f"{upload_folder}/{accountId}/{filename}";
    blob = bucket.blob(blob_name)
    # print("Image format is: ", im_format)
    content_type = 'image/jpeg'
    public = ''
    if im_format=='PNG':
        content_type = 'image/png'
        #pic.convert('RGB')
    obj_str = str(file) + '~' + str(content_type) + '~' + str(blob.size) + '~' + str(width) + '~' + str(height) + '~' + accountId;
    obj_id = f"{hashlib.sha1(str(obj_str).encode()).hexdigest()}"
    file = File(file=obj_id, collection_name=f"{upload_folder}").get();
    if file is not None:
        return file
    blob.upload_from_string(image_bytes, content_type=content_type)
    blob.make_public()
    public = blob.public_url
    # print(blob.metadata)

    if public:
        blob.make_public()
   
    analyzed = None;
    text_detected = None;
    if process is True:
        try:
            analyzed = analyze([blob.public_url]);
            text_detected = text_detection(pic);
        except Exception as e:
            print("Exception catched", e);

    #blob_split  = blob.id.split('/')
    #blob_id = blob_split[len(blob_split)-1]; 
    video_obj = {"file_source": file, "file_url": blob.public_url, "bucket_name": blob.bucket.name, "accountId": accountId, "size": blob.size, "blob_name": blob.name, "created_time": blob.time_created, "blob_id": blob.id, "type": blob.content_type, "width": width, "height": height}
    video_info = pydash.omit(video_obj, ['abs_path', 'abs_path_without_ext', 'parent_path'])
    if analyzed is not None and len(analyzed)>0:
        video_info['colors'] = analyzed;
    if text_detected is not None and len(text_detected)>0:
        video_info['text'] = text_detected;
    #obj_id = f"{hashlib.sha1(str(blob.name + '~' + str(blob.size) + '~' + accountId).encode()).hexdigest()}"
    video_info['_id'] = obj_id
    video_info['id'] = obj_id
    file_create = File(file=video_info, collection_name=f"{upload_folder}").create(**video_info);
    if file_create is None or file_create.status.lower()!="ok":
        return None
    # db = FirebaseCollectionBase(collection_name=collection).db
    # db.collection(collection).document(obj_id).set(video_info, merge=True)
    # return video_info
    return file_create.data


def crop_image(url: str, accountId: str, crop, upload_folder="image_upload", bucket_name="adafri_upload", project_id="adafri-backend"):
    crop_image_data = crop_image_fn(url, crop);
    # print('crop_image_data', crop_image_data)
    file_path = f"{upload_folder}/{accountId}/{crop_image_data['filename']}"
    upload = upload_blob_from_blob_From_File(crop_image_data['abs_path'], bucket_name, file_path, project_id)
    os.remove(crop_image_data['abs_path']);
    print('crop_image_data', crop_image_data)
    print('upload', upload)
    result = pydash.omit({**crop_image_data, **upload}, ['abs_path', 'abs_path_without_ext', 'parent_path']);
    return result

def crop_image_from_url(url: str, accountId: str, crop, upload_folder="image_upload", bucket_name="adafri_upload", id=None, project_id="adafri-backend")->'File | None':
    data = {
        "bucket_name": bucket_name,
        "accountId": accountId,
        "upload_folder": upload_folder,
        "url": url,
        "file_url": url,
        "crop": crop,
    }
    
    file = None
    if id is not None:
        file = File(file=id, collection_name=f"{upload_folder}").get();
        if file is not None:
            if file.url is not None and file.file_url is None:
                file.file_url = file.url
            if file.crop_state == "done":
                return file
    value = crop_image(url, accountId, crop, upload_folder, bucket_name, project_id)
    result = pydash.omit({**value, **data}, ['abs_path', 'abs_path_without_ext', 'parent_path']);
    # video_hash = hashlib.sha1(json.dumps(result).encode()).hexdigest();
    crop_state = "done";
    #result['id'] = video_hash;
    result['crop_state'] = crop_state
    file_create = File(file=result, collection_name=f"{upload_folder}").create(**result);
    if file_create is None or file_create.status.lower()!="ok":
        return None
    # db = FirebaseCollectionBase(collection_name=collection).db
    # db.collection(collection).document(obj_id).set(video_info, merge=True)
    # return video_info
    return file_create.data
    # db.collection(upload_folder).document(doc_init['id']).set({**result}, merge=True)
    # return doc_init | result;
def get_image_bytes(url: str):
    headers = {
        "User-Agent": "Mozilla/5.0"
    }
    resp = requests.get(url, headers=headers, timeout=10)
    resp.raise_for_status()  # Raises an error for bad status codes

    # Check if the response is actually an image
    content_type = resp.headers.get("Content-Type", "")
    if not content_type.startswith("image/"):
        raise ValueError(f"URL did not return an image. Content-Type: {content_type}")

    # Try to open the image
    try:
        # print('rsp', resp.content)
        image_bytes = resp.content
        img = Image.open(BytesIO(resp.content))
        img.verify()  # Verify that it is, in fact, an image
        # If you want to return the image object, reopen it (verify() closes the file)
        img = Image.open(BytesIO(resp.content))
        return image_bytes,img 
    except Exception as e:
        raise ValueError(f"Failed to identify image file: {e}")
    # image_bytes = urlopen(url).read()
    # print("Image size is: ", image_bytes)
    # try:
    #     pic = Image.open(BytesIO(image_bytes))
    #     im_format = pic.format
    #     print("Image format is: ", im_format)
    #     # content_type = 'image/jpeg'
    #     # public = ''
    #     # if im_format=='PNG':
    #     #     content_type = 'image/png'
    #     return image_bytes, pic
    # except Exception as e:
    #     print("Exception catched", e);
    #     return None

def get_compression_estimate(size_kb: int, target_size_kb: int) -> float:
    """
    Get compression ratio needed to achieve target file size.
    Returns compression ratio (0.0 to 1.0, where 1.0 = no compression).
    """
    if not size_kb or size_kb <= 0:
        return 1.0
    
    compression_ratio = target_size_kb / size_kb
    return max(0.1, min(1.0, compression_ratio))

def estimate_pre_crop_file_size(width: int, height: int, current_size_kb: int, target_width: int, target_height: int) -> int:
    """
    Estimate the file size in KB needed before cropping to achieve target dimensions.
    
    Args:
        width, height: Current image dimensions
        current_size_kb: Current file size in KB
        target_width, target_height: Desired crop dimensions
    
    Returns:
        Estimated file size in KB needed before cropping
    """
    try:
        if not width or not height or not current_size_kb:
            return current_size_kb or 0
        
        # Calculate current and target areas
        current_area = width * height
        target_area = target_width * target_height
        
        if current_area <= 0 or target_area <= 0:
            print('current_area <= 0 or target_area <= 0')
            return current_size_kb
        
        # Calculate the ratio of target area to current area
        area_ratio = target_area / current_area
        
        # If target area is larger than current area, we need a bigger image
        if area_ratio > 1:
            # Estimate size needed for the larger area
            estimated_size = int(current_size_kb * area_ratio)
        else:
            print('area_ratio <= 1')
            # If target is smaller, current size should be sufficient
            estimated_size = current_size_kb
        
        return estimated_size
        
    except Exception as e:
        print(f"Error estimating pre-crop file size: {e}")
        return current_size_kb or 0

def calculate_required_image_size(target_width: int, target_height: int, current_width: int, current_height: int) -> tuple[int, int]:
    """
    Calculate the minimum image size needed to achieve target crop dimensions.
    
    Returns:
        Tuple of (required_width, required_height)
    """
    try:
        if not target_width or not target_height:
            return (current_width, current_height)
        
        # Calculate aspect ratios
        target_ratio = target_width / target_height
        current_ratio = current_width / current_height
        
        if target_ratio > current_ratio:
            # Target is wider - need to increase width
            required_width = int(current_height * target_ratio)
            required_height = current_height
        else:
            # Target is taller - need to increase height
            required_width = current_width
            required_height = int(current_width / target_ratio)
        
        return (required_width, required_height)
        
    except Exception as e:
        print(f"Error calculating required image size: {e}")
        return (current_width, current_height)

def estimate_cropped_file_size(width: int, height: int, current_size_kb: int, crop_x: int, crop_y: int, crop_width: int, crop_height: int) -> int:
    """
    Estimate the file size of the cropped portion before actually cropping.
    
    Args:
        width, height: Original image dimensions
        current_size_kb: Original file size in KB
        crop_x, crop_y: Crop starting coordinates
        crop_width, crop_height: Crop dimensions
    
    Returns:
        Estimated file size in KB of the cropped portion
    """
    try:
        if not width or not height or not current_size_kb:
            return 0
        
        # Calculate original and crop areas
        original_area = width * height
        crop_area = crop_width * crop_height
        
        if original_area <= 0 or crop_area <= 0:
            return 0
        
        # Calculate the ratio of crop area to original area
        area_ratio = crop_area / original_area
        
        # Estimate file size based on area ratio
        # File size is roughly proportional to image area
        estimated_crop_size = int(current_size_kb * area_ratio)
        
        return estimated_crop_size
        
    except Exception as e:
        print(f"Error estimating cropped file size: {e}")
        return 0

def estimate_crop_size_from_coordinates(image_width: int, image_height: int, image_size_kb: int, crop_data: dict) -> int:
    """
    Estimate cropped file size from crop coordinates.
    
    Args:
        image_width, image_height: Original image dimensions
        image_size_kb: Original file size in KB
        crop_data: Dict with x, y, width, height coordinates
    
    Returns:
        Estimated file size in KB of the cropped portion
    """
    try:
        crop_x = crop_data.get('x', 0)
        crop_y = crop_data.get('y', 0)
        crop_width = crop_data.get('width', 0)
        crop_height = crop_data.get('height', 0)
        
        return estimate_cropped_file_size(
            image_width, image_height, 
            image_size_kb, 
            crop_x, crop_y, crop_width, crop_height
        )
        
    except Exception as e:
        print(f"Error estimating crop size from coordinates: {e}")
        return 0

# # Usage example:
# original_width, original_height = 1200, 800
# original_size_kb = 800
# crop_coords = {'x': 100, 'y': 100, 'width': 600, 'height': 400}

# estimated_crop_size = estimate_crop_size_from_coordinates(
#     original_width, original_height,
#     original_size_kb, crop_coords
# )

# print(f"Original: {original_width}x{original_height}, {original_size_kb}KB")
# print(f"Crop area: {crop_coords['width']}x{crop_coords['height']}")
# print(f"Estimated cropped file size: {estimated_crop_size}KB")
# # Usage example:
# current_width, current_height = 800, 600
# current_size_kb = 500
# target_width, target_height = 400, 300

# # Estimate size needed before cropping
# estimated_size = estimate_pre_crop_file_size(
#     current_width, current_height, 
#     current_size_kb, 
#     target_width, target_height
# )

# # Calculate required dimensions
# required_width, required_height = calculate_required_image_size(
#     target_width, target_height,
#     current_width, current_height
# )

# print(f"Current: {current_width}x{current_height}, {current_size_kb}KB")
# print(f"Target crop: {target_width}x{target_height}")
# print(f"Required image: {required_width}x{required_height}")
# print(f"Estimated size needed: {estimated_size}KB")


def get_file_size_from_url(url: str) -> int:
    """
    Get accurate file size in KB from URL by downloading in chunks.
    Returns size in KB, or 0 if unable to determine.
    """
    try:
        # Stream the response to get accurate size
        with requests.get(url, stream=True, timeout=30) as response:
            response.raise_for_status()
            
            # Get content length if available
            content_length = response.headers.get('content-length')
            if content_length:
                size_kb = int(content_length)
                return size_kb
            
            # If no content-length, calculate by reading in chunks
            total_size = 0
            chunk_size = 8192  # 8KB chunks
            
            for chunk in response.iter_content(chunk_size=chunk_size):
                if chunk:
                    total_size += len(chunk)
                    # Stop after reading 1MB to avoid downloading huge files
                    if total_size > 1024 * 1024:  # 1MB limit
                        break
            
            return total_size  # Convert to KB
            
    except Exception as e:
        print(f"Error getting file size from URL: {e}")
        return 0

def get_file_size_accurate(url: str) -> int:
    """
    Alternative method using range requests for more accurate size.
    """
    try:
        # Try to get file size using range request
        headers = {'Range': 'bytes=0-1023'}  # Request first 1KB
        response = requests.get(url, headers=headers, timeout=10)
        
        if response.status_code == 206:  # Partial content
            # Parse Content-Range header
            content_range = response.headers.get('content-range', '')
            if '/' in content_range:
                total_size = int(content_range.split('/')[-1])
                return total_size
        
        # Fallback to streaming method
        return get_file_size_from_url(url)
        
    except Exception as e:
        print(f"Error getting accurate file size: {e}")
        return 0
def get_file_info_from_url(url: str) -> dict:
    """
    Get comprehensive file information from URL.
    Returns dict with size, content_type, and other headers.
    """
    try:
        response = requests.head(url, timeout=10, allow_redirects=True)
        response.raise_for_status()
        
        content_length = response.headers.get('content-length', '0')
        size_kb = int(content_length) if content_length != '0' else 0
        
        return {
            'size_kb': size_kb,
            'content_type': response.headers.get('content-type', ''),
            'content_length': content_length,
            'last_modified': response.headers.get('last-modified', ''),
            'etag': response.headers.get('etag', '')
        }
    except Exception as e:
        print(f"Error getting file info from URL: {e}")
        return {'size_kb': 0, 'content_type': '', 'content_length': '0'}