import asyncio
import base64
import logging
import re

import aiohttp
import orjson
import requests
from django.conf import settings
from rest_framework import status

log = logging.getLogger(__name__)
SIGNAL_UUID_PATTERN = re.compile(
    r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
    re.IGNORECASE,
)


def _safe_parse_send_response(payload_value) -> int | bool:
    payload = payload_value
    if isinstance(payload_value, str):
        try:
            payload = orjson.loads(payload_value)
        except orjson.JSONDecodeError:
            return False
    if not isinstance(payload, dict):
        return False
    try:
        ts = payload.get("timestamp")
        return int(ts) if ts else False
    except (TypeError, ValueError):
        return False


def normalize_signal_recipient(recipient: str) -> str:
    raw = str(recipient or "").strip()
    if not raw:
        return ""
    if SIGNAL_UUID_PATTERN.fullmatch(raw):
        return raw
    if raw.startswith("+"):
        digits = re.sub(r"[^0-9]", "", raw)
        return f"+{digits}" if digits else raw
    digits_only = re.sub(r"[^0-9]", "", raw)
    if digits_only and raw.isdigit():
        return f"+{digits_only}"
    return raw


async def start_typing(uuid):
    base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
    url = f"{base}/v1/typing_indicator/{settings.SIGNAL_NUMBER}"
    data = {"recipient": uuid}

    async with aiohttp.ClientSession() as session:
        async with session.put(url, json=data) as response:
            return await response.text()  # Optional: Return response content


async def stop_typing(uuid):
    base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
    url = f"{base}/v1/typing_indicator/{settings.SIGNAL_NUMBER}"
    data = {"recipient": uuid}

    async with aiohttp.ClientSession() as session:
        async with session.delete(url, json=data) as response:
            return await response.text()  # Optional: Return response content


async def download_and_encode_base64(file_url, filename, content_type, session=None):
    """
    Downloads a file from a given URL asynchronously, converts it to Base64,
    and returns it in Signal's expected format.

    Args:
        file_url (str): The URL of the file to download.
        filename (str): The name of the file.
        content_type (str): The MIME type of the file.

    Returns:
        str | None: The Base64 encoded attachment string in Signal's expected format, or None on failure.
    """
    try:
        if session is not None:
            async with session.get(file_url, timeout=10) as response:
                if response.status != 200:
                    return None
                file_data = await response.read()
                base64_encoded = base64.b64encode(file_data).decode("utf-8")
                return (
                    f"data:{content_type};filename={filename};base64,{base64_encoded}"
                )

        async with aiohttp.ClientSession() as local_session:
            async with local_session.get(file_url, timeout=10) as response:
                if response.status != 200:
                    return None

                file_data = await response.read()
                base64_encoded = base64.b64encode(file_data).decode("utf-8")

                # Format according to Signal's expected structure
                return (
                    f"data:{content_type};filename={filename};base64,{base64_encoded}"
                )

    except aiohttp.ClientError:
        # log.error(f"Failed to download file: {file_url}, error: {e}")
        return None


async def send_message_raw(
    recipient_uuid, text=None, attachments=None, metadata=None, detailed=False
):
    """
    Sends a message using the Signal REST API, ensuring attachment links are not included in the text body.

    Args:
        recipient_uuid (str): The UUID of the recipient.
        text (str, optional): The message to send.
        attachments (list, optional): A list of attachment dictionaries with URL, filename, and content_type.

    Returns:
        int | bool: Timestamp if successful, False otherwise.
    """
    base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
    url = f"{base}/v2/send"

    normalized_recipient = normalize_signal_recipient(recipient_uuid)
    data = {
        "recipients": [normalized_recipient],
        "number": settings.SIGNAL_NUMBER,
        "base64_attachments": [],
    }
    meta = dict(metadata or {})

    async def _attachment_to_base64(attachment, session):
        row = dict(attachment or {})
        filename = row.get("filename") or "attachment.bin"
        content_type = row.get("content_type") or "application/octet-stream"
        content = row.get("content")
        if isinstance(content, memoryview):
            content = content.tobytes()
        elif isinstance(content, bytearray):
            content = bytes(content)
        if isinstance(content, bytes):
            encoded = base64.b64encode(content).decode("utf-8")
            return f"data:{content_type};filename={filename};base64,{encoded}"
        file_url = row.get("url")
        if not file_url:
            return None
        return await download_and_encode_base64(
            file_url, filename, content_type, session
        )

    # Asynchronously resolve and encode all attachments
    attachments = attachments or []
    async with aiohttp.ClientSession() as session:
        tasks = [_attachment_to_base64(att, session) for att in attachments]
        encoded_attachments = await asyncio.gather(*tasks)

    # Filter out failed downloads (None values)
    data["base64_attachments"] = [att for att in encoded_attachments if att]

    # Remove the message body if it only contains an attachment link
    attachment_urls = {
        str((att or {}).get("url") or "").strip()
        for att in attachments
        if str((att or {}).get("url") or "").strip()
    }
    if text and text.strip() in attachment_urls:
        # log.info("Removing message body since it only contains an attachment link.")
        text = None  # Don't send the link as text

    if text:
        data["message"] = text

    quote_timestamp = int(meta.get("quote_timestamp") or 0)
    quote_author = str(meta.get("quote_author") or "").strip()
    quote_text = str(meta.get("quote_text") or "").strip()
    has_quote = quote_timestamp > 0 and bool(quote_author)

    payloads = [dict(data)]
    if has_quote:
        flat_quote_payload = dict(data)
        flat_quote_payload["quote_timestamp"] = int(quote_timestamp)
        flat_quote_payload["quote_author"] = quote_author
        if quote_text:
            flat_quote_payload["quote_message"] = quote_text

        nested_quote_payload = dict(data)
        nested_quote_payload["quote"] = {
            "id": int(quote_timestamp),
            "author": quote_author,
        }
        if quote_text:
            nested_quote_payload["quote"]["text"] = quote_text

        payloads = [flat_quote_payload, nested_quote_payload, dict(data)]

    async with aiohttp.ClientSession() as session:
        for index, payload in enumerate(payloads):
            async with session.post(url, json=payload) as response:
                response_text = await response.text()
                response_status = response.status
                if response_status == status.HTTP_201_CREATED:
                    ts = orjson.loads(response_text).get("timestamp", None)
                    return ts if ts else False
                if index == len(payloads) - 1:
                    log.warning(
                        "Signal send failed status=%s recipient=%s body=%s",
                        response_status,
                        normalized_recipient,
                        response_text[:300],
                    )
                    if detailed:
                        return {
                            "ok": False,
                            "status": int(response_status),
                            "error": str(response_text or "").strip()[:500],
                            "recipient": normalized_recipient,
                        }
                    return False
                if response_status not in {
                    status.HTTP_400_BAD_REQUEST,
                    status.HTTP_422_UNPROCESSABLE_ENTITY,
                }:
                    log.warning(
                        "Signal send failed early status=%s recipient=%s body=%s",
                        response_status,
                        normalized_recipient,
                        response_text[:300],
                    )
                    if detailed:
                        return {
                            "ok": False,
                            "status": int(response_status),
                            "error": str(response_text or "").strip()[:500],
                            "recipient": normalized_recipient,
                        }
                    return False
                log.warning(
                    "signal send quote payload rejected (%s), trying fallback shape: %s",
                    response_status,
                    response_text[:200],
                )
    return False


async def send_reaction(
    recipient_uuid,
    emoji,
    target_timestamp=None,
    target_author=None,
    remove=False,
):
    base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
    sender_number = settings.SIGNAL_NUMBER
    normalized_recipient = normalize_signal_recipient(recipient_uuid)
    normalized_target_author = normalize_signal_recipient(
        str(target_author or normalized_recipient)
    )
    if not normalized_recipient or not target_timestamp:
        return False

    payload = {
        "recipient": normalized_recipient,
        "reaction": str(emoji or ""),
        "target_author": normalized_target_author,
        "timestamp": int(target_timestamp),
        "remove": bool(remove),
    }

    candidate_urls = [f"{base}/v1/reactions/{sender_number}"]

    timeout = aiohttp.ClientTimeout(total=20)
    async with aiohttp.ClientSession(timeout=timeout) as session:
        for url in candidate_urls:
            for method in ("post",):
                try:
                    request = getattr(session, method)
                    async with request(url, json=payload) as response:
                        if 200 <= response.status < 300:
                            return True
                except Exception:
                    continue
    return False


async def fetch_signal_attachment(attachment_id):
    """
    Asynchronously fetches an attachment from Signal.

    Args:
        attachment_id (str): The Signal attachment ID.

    Returns:
        dict | None:
            {
                "content": <binary file data>,
                "content_type": <MIME type>,
                "filename": <original filename (if available)>
            }
            or None if the request fails.
    """
    base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
    url = f"{base}/v1/attachments/{attachment_id}"

    try:
        async with aiohttp.ClientSession() as session:
            async with session.get(url, timeout=10) as response:
                if response.status != 200:
                    return None  # Failed request

                content_type = response.headers.get(
                    "Content-Type", "application/octet-stream"
                )
                content = await response.read()
                size = int(response.headers.get("Content-Length", len(content)))

                filename = attachment_id  # Default fallback filename
                content_disposition = response.headers.get("Content-Disposition")
                if content_disposition:
                    parts = content_disposition.split(";")
                    for part in parts:
                        if "filename=" in part:
                            filename = part.split("=", 1)[1].strip().strip('"')

                return {
                    "content": content,
                    "content_type": content_type,
                    "filename": filename,
                    "size": size,
                }
    except aiohttp.ClientError:
        return None  # Network error


def download_and_encode_base64_sync(file_url, filename, content_type):
    """
    Downloads a file from a given URL, converts it to Base64, and returns it in Signal's expected format.

    Args:
        file_url (str): The URL of the file to download.
        filename (str): The name of the file.
        content_type (str): The MIME type of the file.

    Returns:
        str: The Base64 encoded attachment string in Signal's expected format.
    """
    try:
        response = requests.get(file_url, timeout=10)
        response.raise_for_status()

        file_data = response.content
        base64_encoded = base64.b64encode(file_data).decode("utf-8")

        # Format according to Signal's expected structure
        return f"data:{content_type};filename={filename};base64,{base64_encoded}"
    except requests.RequestException:
        # log.error(f"Failed to download file: {file_url}, error: {e}")
        return None


def send_message_raw_sync(recipient_uuid, text=None, attachments=None):
    """
    Sends a message using the Signal REST API, ensuring attachment links are not included in the text body.

    Args:
        recipient_uuid (str): The UUID of the recipient.
        text (str, optional): The message to send.
        attachments (list, optional): A list of attachment dictionaries with URL, filename, and content_type.

    Returns:
        int | bool: Timestamp if successful, False otherwise.
    """
    base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
    url = f"{base}/v2/send"
    data = {
        "recipients": [normalize_signal_recipient(recipient_uuid)],
        "number": settings.SIGNAL_NUMBER,
        "base64_attachments": [],
    }

    attachments = attachments or []

    # Convert attachments to Base64
    for att in attachments:
        base64_data = download_and_encode_base64_sync(
            att["url"], att["filename"], att["content_type"]
        )
        if base64_data:
            data["base64_attachments"].append(base64_data)

    # Remove the message body if it only contains an attachment link
    if text and (text.strip() in [att["url"] for att in attachments]):
        # log.info("Removing message body since it only contains an attachment link.")
        text = None  # Don't send the link as text

    if text:
        data["message"] = text

    try:
        response = requests.post(url, json=data, timeout=10)
        response.raise_for_status()
    except requests.RequestException:
        # log.error(f"Failed to send Signal message: {e}")
        return False

    if (
        response.status_code == status.HTTP_201_CREATED
    ):  # Signal server returns 201 on success
        try:
            payload = response.json()
        except ValueError:
            payload = {}
        return _safe_parse_send_response(payload)
    return False  # If response status is not 201
