from my_modules.decoratory.header import login_required
from my_modules.expiry import parse_expires, ensure_utc
from my_modules.file_meta import iso_stamp_filename, format_size
from quart_common.web.wide_event import add_wide_event_context

from quart import Blueprint, request, jsonify, current_app
import asyncio, hashlib

upload_bp = Blueprint('upload_bp', __name__)

# --- Helpers -----------------------------------------------------

async def read_all(uploaded) -> bytes:
  """Read all bytes from an uploaded file, handling sync or async .read()."""
  reader = getattr(uploaded, 'read', None)
  if reader is None:
    return b''
  if asyncio.iscoroutinefunction(reader):
    return await reader()

  data = reader()
  if asyncio.iscoroutine(data):
    return await data
  return data


async def fingerprint_stream(stream, chunk_size:int=1024 * 1024) -> tuple[str|None, int|None]:
  if not hasattr(stream, 'seek') or not hasattr(stream, 'tell'):
    return None, None

  try:
    stream.seek(0)
  except Exception:
    return None, None

  digest = hashlib.sha256()
  size_bytes = 0

  while True:
    chunk = await asyncio.to_thread(stream.read, chunk_size)
    if not chunk:
      break
    size_bytes += len(chunk)
    digest.update(chunk)

  try:
    stream.seek(0)
  except Exception:
    return None, None

  return digest.hexdigest(), size_bytes


def fingerprint_bytes(data: bytes) -> str:
  return hashlib.sha256(data).hexdigest()


# --- Routes ------------------------------------------------------

@upload_bp.post('/api/upload')
@login_required
async def api_upload(user):
  """
  POST /upload/api/upload
  Accepts:
    - multipart form with 'file' or 'text'
    - 'expires' can be '1h', '7d', or ISO timestamp
    - 'note' optional
  """
  form = await request.form
  files = await request.files
  note = form.get('note', '')
  expires_raw = form.get('expires', '')
  text = form.get('text', '')
  orphan_registry = getattr(current_app, 'orphan_storage_registry', None)

  add_wide_event_context(nanoshare={"operation": "upload", "has_file": bool(files.get('file')), "has_text": bool(text.strip())})

  uploaded = files.get('file')
  expires_at_dt = ensure_utc(parse_expires(expires_raw))

  if not uploaded and not text.strip():
    add_wide_event_context(nanoshare={"operation_status": "missing_content"})
    return jsonify({'ok': False, 'error': 'No content provided'}), 400

  content_type = None

  # --- binary upload path ---
  if uploaded:
    fname = uploaded.filename or ''
    add_wide_event_context(nanoshare={"upload_type": "file", "filename_present": bool(fname)})
    ctype = uploaded.mimetype or 'application/octet-stream'
    content_type = ctype
    storage_id = None
    size_bytes = 0
    fingerprint = None
    reused_orphan_storage_id = False

    # generate filename if missing/placeholder
    if not fname or fname.lower() in {'blob', 'file'}:
      ext = {
        'image/png': 'png',
        'image/jpeg': 'jpg',
        'image/gif': 'gif',
        'image/webp': 'webp',
        'application/pdf': 'pdf',
        'text/plain': 'txt',
      }.get(ctype, 'bin')
      fname = iso_stamp_filename('pasted', ext)

    stream = getattr(uploaded, 'stream', None)

    if stream is not None:
      fingerprint, detected_size = await fingerprint_stream(stream)
      size_bytes = detected_size or 0
      storage_id = (
        await orphan_registry.pop_recent(user['sub'], fingerprint)
        if orphan_registry
        else None
      )
      if storage_id:
        reused_orphan_storage_id = True
        add_wide_event_context(nanoshare={"reused_orphan_storage_id": True})
      else:
        storage_id, sent_size = await current_app.convex.send_stream_to_storage(stream=stream,content_type=content_type)
        size_bytes = sent_size
    else:
      data = await read_all(uploaded)
      fingerprint = fingerprint_bytes(data)
      storage_id = (
        await orphan_registry.pop_recent(user['sub'], fingerprint)
        if orphan_registry
        else None
      )
      if storage_id:
        reused_orphan_storage_id = True
        add_wide_event_context(nanoshare={"reused_orphan_storage_id": True})
      else:
        storage_id = await current_app.convex.send_to_storage(data=data, content_type=content_type)
      size_bytes = len(data)

    file_size_pretty = format_size(size_bytes)

    try:
      await current_app.convex.add_file(
        file_name=fname,
        file_size=file_size_pretty,
        note=note,
        content_type=content_type,
        expires_at=expires_at_dt,
        storage_id=storage_id,
        user_id=user['sub'],
      )
    except Exception:
      if storage_id and not reused_orphan_storage_id and orphan_registry:
        await orphan_registry.remember(user['sub'], fingerprint, storage_id)
      raise

  # --- text upload path ---
  elif text.strip():
    add_wide_event_context(nanoshare={"upload_type": "text"})
    data = text.encode('utf-8')
    fname = iso_stamp_filename('pasted', 'txt')
    fingerprint = fingerprint_bytes(data)
    storage_id = (
      await orphan_registry.pop_recent(user['sub'], fingerprint)
      if orphan_registry
      else None
    )
    reused_orphan_storage_id = bool(storage_id)
    if reused_orphan_storage_id:
      add_wide_event_context(nanoshare={"reused_orphan_storage_id": True})
    if not storage_id:
      storage_id = await current_app.convex.send_to_storage(
        data=data, content_type='text/plain'
      )

    size_bytes = len(data)
    file_size_pretty = format_size(size_bytes)

    try:
      await current_app.convex.add_file(
        file_name=fname,
        file_size=file_size_pretty,
        note=note,
        content_type='text/plain',
        expires_at=expires_at_dt,
        storage_id=storage_id,
        user_id=user['sub'],
      )
    except Exception:
      if not reused_orphan_storage_id and orphan_registry:
        await orphan_registry.remember(user['sub'], fingerprint, storage_id)
      raise

  add_wide_event_context(nanoshare={"operation_status": "uploaded", "content_type": content_type})
  return jsonify({'ok': True})
