"""
Bulk import CourseQuestion rows from CSV or Excel.
Does not invent content — only imports what reviewers upload.
"""
from __future__ import annotations

import csv
import io
from dataclasses import dataclass

from django.utils import timezone

from apps.courses.models import (
    CORRECT_OPTION_CHOICES,
    Course,
    CourseQuestion,
    DIFFICULTY_EASY,
    DIFFICULTY_HARD,
    DIFFICULTY_MEDIUM,
    QUESTION_DIFFICULTY_CHOICES,
)

VALID_CORRECT = {c[0] for c in CORRECT_OPTION_CHOICES}
VALID_DIFFICULTY = {c[0] for c in QUESTION_DIFFICULTY_CHOICES}

# Canonical CSV/Excel headers (case-insensitive match)
REQUIRED_HEADERS = (
    'question_text',
    'option_a',
    'option_b',
    'option_c',
    'option_d',
    'correct_option',
)
OPTIONAL_HEADERS = (
    'difficulty',
    'topic_tag',
    'is_active',
    'is_verified',
)

CSV_TEMPLATE_HEADERS = REQUIRED_HEADERS + OPTIONAL_HEADERS


@dataclass
class ImportResult:
    created: int = 0
    skipped: int = 0
    errors: list[str] | None = None

    def __post_init__(self):
        if self.errors is None:
            self.errors = []


def _norm_header(h: str) -> str:
    return (h or '').strip().lower().replace(' ', '_')


def _truthy(value) -> bool:
    if value is None:
        return False
    return str(value).strip().lower() in ('1', 'true', 'yes', 'y', 'on')


def _normalize_row(row: dict) -> dict:
    return {_norm_header(k): (v.strip() if isinstance(v, str) else v) for k, v in row.items() if k}


def rows_from_csv(file_obj) -> list[dict]:
    raw = file_obj.read()
    if isinstance(raw, bytes):
        text = raw.decode('utf-8-sig')
    else:
        text = raw
    reader = csv.DictReader(io.StringIO(text))
    return [_normalize_row(r) for r in reader]


def rows_from_excel(file_obj) -> list[dict]:
    try:
        import openpyxl
    except ImportError as exc:
        raise ImportError(
            'Excel import requires openpyxl. Install with: pip install openpyxl'
        ) from exc
    wb = openpyxl.load_workbook(file_obj, read_only=True, data_only=True)
    ws = wb.active
    rows_iter = ws.iter_rows(values_only=True)
    headers = [_norm_header(str(h or '')) for h in next(rows_iter)]
    out = []
    for values in rows_iter:
        if not values or all(v is None or str(v).strip() == '' for v in values):
            continue
        row = {}
        for i, h in enumerate(headers):
            if not h:
                continue
            val = values[i] if i < len(values) else ''
            row[h] = '' if val is None else str(val).strip()
        out.append(row)
    return out


def parse_upload(file_obj, filename: str) -> list[dict]:
    name = (filename or '').lower()
    if name.endswith(('.xlsx', '.xlsm')):
        return rows_from_excel(file_obj)
    return rows_from_csv(file_obj)


def clear_course_question_bank(course: Course) -> dict:
    """
    Remove a course's bank for re-import.
    Questions already used in an exam attempt are PROTECTed — those are
    deactivated instead of deleted so audit history stays intact.
    """
    qs = CourseQuestion.objects.filter(course=course)
    deletable = qs.filter(attempt_appearances__isnull=True)
    deleted_count, _ = deletable.delete()
    deactivated = CourseQuestion.objects.filter(course=course).update(
        is_active=False,
        is_verified=False,
    )
    return {'deleted': deleted_count, 'deactivated': deactivated}


def import_questions(
    course: Course,
    rows: list[dict],
    *,
    created_by=None,
    mark_verified: bool = False,
) -> ImportResult:
    result = ImportResult()
    to_create = []

    for i, row in enumerate(rows, start=2):  # row 1 = header
        missing = [h for h in REQUIRED_HEADERS if not row.get(h)]
        if missing:
            result.skipped += 1
            result.errors.append(f'Row {i}: missing {", ".join(missing)}')
            continue

        correct = str(row.get('correct_option', '')).strip().upper()
        if correct not in VALID_CORRECT:
            result.skipped += 1
            result.errors.append(f'Row {i}: correct_option must be A, B, C, or D')
            continue

        difficulty = str(row.get('difficulty') or DIFFICULTY_MEDIUM).strip().lower()
        if difficulty not in VALID_DIFFICULTY:
            result.skipped += 1
            result.errors.append(f'Row {i}: difficulty must be easy, medium, or hard')
            continue

        is_active = True if row.get('is_active') in (None, '') else _truthy(row.get('is_active'))
        if 'is_verified' in row and row.get('is_verified') not in (None, ''):
            is_verified = _truthy(row.get('is_verified'))
        else:
            is_verified = mark_verified

        obj = CourseQuestion(
            course=course,
            question_text=str(row['question_text']).strip(),
            option_a=str(row['option_a']).strip()[:500],
            option_b=str(row['option_b']).strip()[:500],
            option_c=str(row['option_c']).strip()[:500],
            option_d=str(row['option_d']).strip()[:500],
            correct_option=correct,
            difficulty=difficulty,
            topic_tag=str(row.get('topic_tag') or '').strip()[:80],
            is_active=is_active,
            is_verified=is_verified,
            created_by=created_by,
            last_reviewed_at=timezone.now() if is_verified else None,
        )
        to_create.append(obj)

    if to_create:
        CourseQuestion.objects.bulk_create(to_create, batch_size=200)
        result.created = len(to_create)
    return result


def csv_template_content() -> str:
    buf = io.StringIO()
    writer = csv.writer(buf)
    writer.writerow(CSV_TEMPLATE_HEADERS)
    writer.writerow([
        'What does SEO stand for?',
        'Search Engine Optimization',
        'Social Engagement Output',
        'Server Endpoint Operation',
        'Site Editing Option',
        'A',
        'easy',
        'on-page',
        'true',
        'false',
    ])
    return buf.getvalue()
