import os
import re

from django.contrib.contenttypes.models import ContentType

from attachments.models import Attachment
from tcms.bugs.models import Bug
from tcms.testruns.models import TestExecution, TestRun


FILE_CATEGORIES = {
    "image": {"jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "ico"},
    "text": {
        "txt", "log", "csv", "json", "xml", "yaml", "yml",
        "html", "htm", "css", "js", "py", "sh", "ini", "cfg", "conf", "toml",
    },
    "markdown": {"md"},
    "pdf": {"pdf"},
    "document": {"docx", "xlsx", "xls", "doc", "ppt", "pptx"},
    "video": {"mp4", "webm", "ogg", "avi", "mov"},
    "audio": {"mp3", "wav", "flac", "aac"},
}

FILE_ICON_CLASSES = {
    "image": "fa fa-file-image-o",
    "text": "fa fa-file-text-o",
    "markdown": "fa fa-file-text-o",
    "pdf": "fa fa-file-pdf-o",
    "document": "fa fa-file-word-o",
    "video": "fa fa-file-video-o",
    "audio": "fa fa-file-audio-o",
    "other": "fa fa-file-o",
}


def get_file_extension(filename):
    """Return lowercase file extension without the dot."""
    _, ext = os.path.splitext(filename)
    return ext.lstrip(".").lower()


def get_file_category(filename):
    """Classify a file into a category based on its extension."""
    ext = get_file_extension(filename)
    for category, extensions in FILE_CATEGORIES.items():
        if ext in extensions:
            return category
    return "other"


def get_file_icon_class(category):
    """Return a FontAwesome icon class for a file category."""
    return FILE_ICON_CLASSES.get(category, FILE_ICON_CLASSES["other"])


def format_file_size(size_bytes):
    """Format a file size in bytes to a human-readable string."""
    if size_bytes is None or size_bytes < 0:
        return "0 B"
    if size_bytes < 1024:
        return "{} B".format(size_bytes)
    if size_bytes < 1024 * 1024:
        return "{:.1f} KB".format(size_bytes / 1024)
    if size_bytes < 1024 * 1024 * 1024:
        return "{:.1f} MB".format(size_bytes / (1024 * 1024))
    return "{:.1f} GB".format(size_bytes / (1024 * 1024 * 1024))


def sanitize_path(name):
    """Sanitize a string for use as a filesystem path component."""
    name = re.sub(r'[<>:"/\\|?*]', "_", name)
    name = name.strip(". ")
    return name[:100] or "unnamed"


def _attachment_to_dict(att, source_type, **extra):
    """Convert an Attachment model instance to a serializable dict."""
    filename = att.filename
    try:
        size = att.attachment_file.size
    except (FileNotFoundError, OSError):
        size = 0

    category = get_file_category(filename)
    result = {
        "id": att.pk,
        "filename": filename,
        "url": att.attachment_file.url,
        "file_type": get_file_extension(filename),
        "file_category": category,
        "file_icon": get_file_icon_class(category),
        "size": size,
        "size_display": format_file_size(size),
        "creator": att.creator.username if att.creator else "",
        "created": att.created.strftime("%Y-%m-%d %H:%M") if att.created else "",
        "source_type": source_type,
    }
    result.update(extra)
    return result


def gather_run_attachments(run_id):
    """
    Gather all attachments for a test run using 3 bulk queries.

    Returns a grouped structure with run-level and per-execution attachments.
    """
    try:
        run = TestRun.objects.select_related("plan").get(pk=run_id)
    except TestRun.DoesNotExist:
        return None

    executions = list(
        TestExecution.objects.filter(run=run)
        .select_related("case", "status")
        .order_by("case__summary")
    )

    run_ct = ContentType.objects.get_for_model(TestRun)
    exec_ct = ContentType.objects.get_for_model(TestExecution)
    bug_ct = ContentType.objects.get_for_model(Bug)

    # Import TestCase model for case-level attachments
    from tcms.testcases.models import TestCase
    case_ct = ContentType.objects.get_for_model(TestCase)

    exec_ids = [str(e.pk) for e in executions]
    exec_pks = [e.pk for e in executions]
    case_ids = list({str(e.case_id) for e in executions if e.case_id})

    # Bulk queries for run, execution, case attachments
    run_attachments = list(
        Attachment.objects.filter(
            content_type=run_ct, object_id=str(run_id)
        ).select_related("creator")
    )

    exec_attachments = []
    if exec_ids:
        exec_attachments = list(
            Attachment.objects.filter(
                content_type=exec_ct, object_id__in=exec_ids
            ).select_related("creator")
        )

    case_attachments = []
    if case_ids:
        case_attachments = list(
            Attachment.objects.filter(
                content_type=case_ct, object_id__in=case_ids
            ).select_related("creator")
        )

    # Bug attachments: query M2M through table for exec→bug links
    bug_links = []
    if exec_pks:
        bug_links = list(
            Bug.executions.through.objects.filter(
                testexecution_id__in=exec_pks
            ).values_list("testexecution_id", "bug_id")
        )

    exec_bug_map = {}  # exec_id → set of bug_ids
    all_bug_ids = set()
    for exec_id, bug_id in bug_links:
        exec_bug_map.setdefault(exec_id, set()).add(bug_id)
        all_bug_ids.add(bug_id)

    bug_info_map = {}  # bug_id → {summary, status}
    if all_bug_ids:
        for bug in Bug.objects.filter(pk__in=all_bug_ids).only("pk", "summary", "status"):
            bug_info_map[bug.pk] = {
                "summary": bug.summary,
                "status": "open" if bug.status else "closed",
            }

    bug_attachments = []
    if all_bug_ids:
        bug_attachments = list(
            Attachment.objects.filter(
                content_type=bug_ct,
                object_id__in=[str(b) for b in all_bug_ids],
            ).select_related("creator")
        )

    bug_att_map = {}
    for att in bug_attachments:
        bug_att_map.setdefault(att.object_id, []).append(att)

    # Index attachments by object_id
    exec_att_map = {}
    for att in exec_attachments:
        exec_att_map.setdefault(att.object_id, []).append(att)

    case_att_map = {}
    for att in case_attachments:
        case_att_map.setdefault(att.object_id, []).append(att)

    groups = []
    total_attachments = 0
    total_size = 0

    # Run-level group
    if run_attachments:
        run_att_dicts = [_attachment_to_dict(a, "run") for a in run_attachments]
        total_attachments += len(run_att_dicts)
        total_size += sum(a["size"] for a in run_att_dicts)
        groups.append({
            "source": "run",
            "source_label": "Test Run #{}".format(run.pk),
            "attachments": run_att_dicts,
        })

    # Per-execution groups (merge execution + case + bug attachments)
    bugs_shown = set()  # track which bugs already appeared in execution groups
    for execution in executions:
        exec_atts = exec_att_map.get(str(execution.pk), [])
        case_atts = case_att_map.get(str(execution.case_id), []) if execution.case_id else []

        # Collect bug attachments linked to this execution
        exec_bug_atts = []  # list of (attachment, bug_id)
        exec_bug_ids = exec_bug_map.get(execution.pk, set())
        for bid in exec_bug_ids:
            for a in bug_att_map.get(str(bid), []):
                exec_bug_atts.append((a, bid))
                bugs_shown.add(bid)

        if not exec_atts and not case_atts and not exec_bug_atts:
            continue

        att_dicts = []
        for a in exec_atts:
            att_dicts.append(_attachment_to_dict(a, "execution"))
        for a in case_atts:
            att_dicts.append(_attachment_to_dict(a, "case"))
        for a, bid in exec_bug_atts:
            info = bug_info_map.get(bid, {})
            att_dicts.append(_attachment_to_dict(
                a, "bug",
                bug_id=bid,
                bug_summary=info.get("summary", ""),
            ))

        total_attachments += len(att_dicts)
        total_size += sum(a["size"] for a in att_dicts)

        status_name = execution.status.name if execution.status else ""
        status_color = execution.status.color if execution.status else "#888"

        groups.append({
            "source": "execution",
            "execution_id": execution.pk,
            "case_id": execution.case_id,
            "case_summary": execution.case.summary if execution.case else "",
            "status": status_name,
            "status_color": status_color,
            "attachments": att_dicts,
        })

    # Standalone bug groups for bugs with attachments not shown above
    for bug_id in all_bug_ids:
        if bug_id in bugs_shown:
            continue
        bug_atts = bug_att_map.get(str(bug_id), [])
        if not bug_atts:
            continue
        info = bug_info_map.get(bug_id, {})
        att_dicts = [
            _attachment_to_dict(
                a, "bug",
                bug_id=bug_id,
                bug_summary=info.get("summary", ""),
            ) for a in bug_atts
        ]
        total_attachments += len(att_dicts)
        total_size += sum(a["size"] for a in att_dicts)
        groups.append({
            "source": "bug",
            "bug_id": bug_id,
            "bug_summary": info.get("summary", ""),
            "bug_status": info.get("status", ""),
            "attachments": att_dicts,
        })

    return {
        "run_id": run.pk,
        "run_summary": run.summary,
        "total_attachments": total_attachments,
        "total_size": total_size,
        "total_size_display": format_file_size(total_size),
        "groups": groups,
    }


def gather_bug_attachments(bug_id):
    """
    Gather all attachments for a bug.

    Returns a structure with bug-level attachments and linked run context.
    """
    try:
        bug = Bug.objects.select_related("product").get(pk=bug_id)
    except Bug.DoesNotExist:
        return None

    bug_ct = ContentType.objects.get_for_model(Bug)

    bug_attachments = list(
        Attachment.objects.filter(
            content_type=bug_ct, object_id=str(bug_id)
        ).select_related("creator")
    )

    # Get linked executions for context (plans + runs + cases)
    linked_plans = []
    linked_runs = []
    linked_cases = []
    seen_plan_ids = set()
    seen_run_ids = set()
    seen_case_ids = set()
    for execution in (
        bug.executions
        .select_related("run__plan", "case")
        .order_by("run__plan__name", "run__summary")
    ):
        run = execution.run
        plan = run.plan if run else None
        if plan and plan.pk not in seen_plan_ids:
            seen_plan_ids.add(plan.pk)
            linked_plans.append({
                "plan_id": plan.pk,
                "plan_name": plan.name,
            })
        if run and run.pk not in seen_run_ids:
            seen_run_ids.add(run.pk)
            linked_runs.append({
                "run_id": run.pk,
                "run_summary": run.summary,
            })
        case = execution.case
        if case and case.pk not in seen_case_ids:
            seen_case_ids.add(case.pk)
            linked_cases.append({
                "case_id": case.pk,
                "case_summary": case.summary,
            })

    total_attachments = 0
    total_size = 0
    groups = []

    if bug_attachments:
        att_dicts = [_attachment_to_dict(a, "bug") for a in bug_attachments]
        total_attachments = len(att_dicts)
        total_size = sum(a["size"] for a in att_dicts)
        groups.append({
            "source": "bug",
            "bug_id": bug.pk,
            "bug_summary": bug.summary,
            "bug_status": "open" if bug.status else "closed",
            "source_label": "BUG-{}: {}".format(bug.pk, bug.summary),
            "attachments": att_dicts,
        })

    return {
        "bug_id": bug.pk,
        "bug_summary": bug.summary,
        "bug_status": "open" if bug.status else "closed",
        "product_name": bug.product.name if bug.product else "",
        "linked_plans": linked_plans,
        "linked_runs": linked_runs,
        "linked_cases": linked_cases,
        "total_attachments": total_attachments,
        "total_size": total_size,
        "total_size_display": format_file_size(total_size),
        "groups": groups,
    }


def build_zip_path(run_summary, case_summary, filename, is_run_level=False):
    """Build a structured path for a file inside a ZIP archive."""
    run_dir = sanitize_path(run_summary)
    if is_run_level:
        return "{}/run_attachments/{}".format(run_dir, filename)
    case_dir = sanitize_path(case_summary) if case_summary else "unknown_case"
    return "{}/{}/{}".format(run_dir, case_dir, filename)
