from decimal import Decimal, InvalidOperation

from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone
from django.utils.dateparse import parse_date, parse_datetime

from crm.constants import ACTIVITY_TYPES, OUTCOMES, PRIORITIES, STAGES, SYSTEM_TYPES
from crm.models import Activity, Contact, Lead

from .cache import invalidate_finder_cache
from .models import School

BACKUP_FORMAT = "school-portal-full-backup"
BACKUP_VERSION = 1
SCHOOL_UPDATE_FIELDS = [
    "name", "district_id", "district_name", "municipality", "school_type",
    "total_students", "up_to_class", "data", "deleted_at",
]


class BackupError(ValueError):
    pass


def _iso(value):
    return value.isoformat() if value is not None else None


def build_backup():
    schools = School.all_objects.order_by("organization_id").select_related(
        "lead__owner"
    ).prefetch_related("people", "lead__activities__created_by")
    records = []
    for school in schools.iterator(chunk_size=500):
        try:
            lead = school.lead
        except Lead.DoesNotExist:
            lead = None

        lead_data = None
        activities = []
        if lead:
            lead_data = {
                "stage": lead.stage,
                "priority": lead.priority,
                "owner_username": lead.owner.get_username() if lead.owner else None,
                "last_contacted_at": _iso(lead.last_contacted_at),
                "attempts": lead.attempts,
                "next_followup": _iso(lead.next_followup),
                "next_action": lead.next_action,
                "deal_value": str(lead.deal_value) if lead.deal_value is not None else None,
                "lost_reason": lead.lost_reason,
                "stage_changed_at": _iso(lead.stage_changed_at),
                "created_at": _iso(lead.created_at),
                "updated_at": _iso(lead.updated_at),
            }
            activities = [
                {
                    "kind": activity.kind,
                    "outcome": activity.outcome,
                    "notes": activity.notes,
                    "occurred_at": _iso(activity.occurred_at),
                    "created_by_username": activity.created_by.get_username()
                    if activity.created_by else None,
                    "created_at": _iso(activity.created_at),
                }
                for activity in lead.activities.all()
            ]

        records.append({
            "organization_id": school.organization_id,
            "name": school.name,
            "district_id": school.district_id,
            "district_name": school.district_name,
            "municipality": school.municipality,
            "school_type": school.school_type,
            "total_students": school.total_students,
            "up_to_class": school.up_to_class,
            "data": school.data,
            "deleted_at": _iso(school.deleted_at),
            "lead": lead_data,
            "activities": activities,
            "contacts": [
                {
                    "name": contact.name,
                    "role": contact.role,
                    "phone": contact.phone,
                    "email": contact.email,
                    "is_primary": contact.is_primary,
                }
                for contact in school.people.all()
            ],
        })

    return {
        "format": BACKUP_FORMAT,
        "version": BACKUP_VERSION,
        "exported_at": timezone.now().isoformat(),
        "schools": records,
    }


def _as_dict(value, label):
    if not isinstance(value, dict):
        raise BackupError(f"{label} must be an object.")
    return value


def _as_list(value, label):
    if not isinstance(value, list):
        raise BackupError(f"{label} must be a list.")
    return value


def _text(value, label, allow_empty=True):
    if not isinstance(value, str) or (not allow_empty and not value):
        raise BackupError(f"{label} must be { 'text' if allow_empty else 'non-empty text' }.")
    return value


def _integer(value, label, nullable=False):
    if value is None and nullable:
        return None
    if isinstance(value, bool):
        raise BackupError(f"{label} must be an integer.")
    try:
        return int(value)
    except (TypeError, ValueError):
        raise BackupError(f"{label} must be an integer.") from None


def _date(value, label, nullable=False):
    if value is None and nullable:
        return None
    parsed = parse_date(value) if isinstance(value, str) else None
    if parsed is None:
        raise BackupError(f"{label} must be an ISO date.")
    return parsed


def _datetime(value, label, nullable=False):
    if value is None and nullable:
        return None
    parsed = parse_datetime(value) if isinstance(value, str) else None
    if parsed is None:
        raise BackupError(f"{label} must be an ISO datetime.")
    if timezone.is_naive(parsed) and timezone.is_aware(timezone.now()):
        parsed = timezone.make_aware(parsed, timezone.get_current_timezone())
    return parsed


def _username(value, label, usernames):
    if value is None:
        return None
    username = _text(value, label, allow_empty=False)
    usernames.add(username)
    return username


def _validate_backup(backup):
    backup = _as_dict(backup, "Backup")
    if backup.get("format") != BACKUP_FORMAT or backup.get("version") != BACKUP_VERSION:
        raise BackupError("This is not a supported school portal backup.")

    raw_schools = _as_list(backup.get("schools"), "schools")
    plans, usernames, organization_ids = [], set(), set()
    stage_values = {value for value, _ in STAGES}
    priority_values = {value for value, _ in PRIORITIES}
    activity_values = {value for value, _ in ACTIVITY_TYPES + SYSTEM_TYPES}
    outcome_values = {value for value, _ in OUTCOMES}

    for index, raw_school in enumerate(raw_schools, 1):
        label = f"schools[{index}]"
        raw_school = _as_dict(raw_school, label)
        organization_id = _integer(raw_school.get("organization_id"), f"{label}.organization_id")
        if organization_id in organization_ids:
            raise BackupError(f"Duplicate organization ID {organization_id} in backup.")
        organization_ids.add(organization_id)
        data = raw_school.get("data")
        if not isinstance(data, dict):
            raise BackupError(f"{label}.data must be an object.")

        school_fields = {
            "name": _text(raw_school.get("name"), f"{label}.name", allow_empty=False),
            "district_id": _integer(raw_school.get("district_id"), f"{label}.district_id", nullable=True),
            "district_name": _text(raw_school.get("district_name"), f"{label}.district_name"),
            "municipality": _text(raw_school.get("municipality"), f"{label}.municipality"),
            "school_type": _text(raw_school.get("school_type"), f"{label}.school_type"),
            "total_students": _integer(raw_school.get("total_students"), f"{label}.total_students", nullable=True),
            "up_to_class": _integer(raw_school.get("up_to_class"), f"{label}.up_to_class", nullable=True),
            "data": data,
            "deleted_at": _datetime(raw_school.get("deleted_at"), f"{label}.deleted_at", nullable=True),
        }

        raw_lead = raw_school.get("lead")
        lead = None
        if raw_lead is not None:
            raw_lead = _as_dict(raw_lead, f"{label}.lead")
            stage = _text(raw_lead.get("stage"), f"{label}.lead.stage", allow_empty=False)
            priority = _text(raw_lead.get("priority"), f"{label}.lead.priority")
            if stage not in stage_values or priority not in priority_values:
                raise BackupError(f"{label} has an unknown CRM stage or priority.")
            deal_value = raw_lead.get("deal_value")
            try:
                deal_value = Decimal(str(deal_value)) if deal_value is not None else None
            except InvalidOperation:
                raise BackupError(f"{label}.lead.deal_value must be a decimal.") from None
            lead = {
                "stage": stage,
                "priority": priority,
                "owner_username": _username(raw_lead.get("owner_username"), f"{label}.lead.owner_username", usernames),
                "last_contacted_at": _datetime(raw_lead.get("last_contacted_at"), f"{label}.lead.last_contacted_at", nullable=True),
                "attempts": _integer(raw_lead.get("attempts"), f"{label}.lead.attempts"),
                "next_followup": _date(raw_lead.get("next_followup"), f"{label}.lead.next_followup", nullable=True),
                "next_action": _text(raw_lead.get("next_action"), f"{label}.lead.next_action"),
                "deal_value": deal_value,
                "lost_reason": _text(raw_lead.get("lost_reason"), f"{label}.lead.lost_reason"),
                "stage_changed_at": _datetime(raw_lead.get("stage_changed_at"), f"{label}.lead.stage_changed_at"),
                "created_at": _datetime(raw_lead.get("created_at"), f"{label}.lead.created_at"),
                "updated_at": _datetime(raw_lead.get("updated_at"), f"{label}.lead.updated_at"),
            }

        raw_activities = _as_list(raw_school.get("activities"), f"{label}.activities")
        if raw_activities and lead is None:
            raise BackupError(f"{label} has CRM activities but no lead.")
        activities = []
        for activity_index, raw_activity in enumerate(raw_activities, 1):
            activity_label = f"{label}.activities[{activity_index}]"
            raw_activity = _as_dict(raw_activity, activity_label)
            kind = _text(raw_activity.get("kind"), f"{activity_label}.kind", allow_empty=False)
            outcome = _text(raw_activity.get("outcome"), f"{activity_label}.outcome")
            if kind not in activity_values or outcome not in outcome_values:
                raise BackupError(f"{activity_label} has an unknown type or outcome.")
            activities.append({
                "kind": kind,
                "outcome": outcome,
                "notes": _text(raw_activity.get("notes"), f"{activity_label}.notes"),
                "occurred_at": _datetime(raw_activity.get("occurred_at"), f"{activity_label}.occurred_at"),
                "created_by_username": _username(raw_activity.get("created_by_username"), f"{activity_label}.created_by_username", usernames),
                "created_at": _datetime(raw_activity.get("created_at"), f"{activity_label}.created_at"),
            })

        contacts = []
        for contact_index, raw_contact in enumerate(_as_list(raw_school.get("contacts"), f"{label}.contacts"), 1):
            contact_label = f"{label}.contacts[{contact_index}]"
            raw_contact = _as_dict(raw_contact, contact_label)
            is_primary = raw_contact.get("is_primary")
            if not isinstance(is_primary, bool):
                raise BackupError(f"{contact_label}.is_primary must be true or false.")
            contacts.append({
                "name": _text(raw_contact.get("name"), f"{contact_label}.name", allow_empty=False),
                "role": _text(raw_contact.get("role"), f"{contact_label}.role"),
                "phone": _text(raw_contact.get("phone"), f"{contact_label}.phone"),
                "email": _text(raw_contact.get("email"), f"{contact_label}.email"),
                "is_primary": is_primary,
            })

        plans.append({
            "organization_id": organization_id,
            "school_fields": school_fields,
            "lead": lead,
            "activities": activities,
            "contacts": contacts,
        })

    User = get_user_model()
    username_field = User.USERNAME_FIELD
    if usernames:
        users = {
            user.get_username(): user
            for user in User.objects.filter(**{f"{username_field}__in": usernames})
        }
    else:
        users = {}
    missing_usernames = usernames - users.keys()
    if missing_usernames:
        missing = ", ".join(sorted(missing_usernames))
        raise BackupError(f"Create or rename these user accounts before importing: {missing}.")
    return plans, users


def restore_backup(backup):
    plans, users = _validate_backup(backup)
    organization_ids = [plan["organization_id"] for plan in plans]
    if not plans:
        return {"schools": 0, "leads": 0, "activities": 0, "contacts": 0}

    with transaction.atomic():
        existing_schools = {
            school.organization_id: school
            for school in School.all_objects.filter(organization_id__in=organization_ids)
        }
        new_schools, changed_schools = [], []
        for plan in plans:
            school = existing_schools.get(plan["organization_id"])
            if school is None:
                school = School(organization_id=plan["organization_id"], **plan["school_fields"])
                new_schools.append(school)
            else:
                for field, value in plan["school_fields"].items():
                    setattr(school, field, value)
                changed_schools.append(school)
        School.objects.bulk_create(new_schools, batch_size=1000)
        School.objects.bulk_update(changed_schools, SCHOOL_UPDATE_FIELDS, batch_size=1000)
        schools_by_id = {
            school.organization_id: school
            for school in School.all_objects.filter(organization_id__in=organization_ids)
        }
        school_pks = [school.pk for school in schools_by_id.values()]

        Contact.objects.filter(school_id__in=school_pks).delete()
        Lead.objects.filter(school_id__in=school_pks).delete()

        lead_records = []
        lead_timestamps = {}
        for plan in plans:
            if plan["lead"] is None:
                continue
            values = dict(plan["lead"])
            owner_username = values.pop("owner_username")
            created_at, updated_at = values["created_at"], values["updated_at"]
            values["owner"] = users.get(owner_username)
            lead_records.append(Lead(school=schools_by_id[plan["organization_id"]], **values))
            lead_timestamps[plan["organization_id"]] = (created_at, updated_at)
        Lead.objects.bulk_create(lead_records, batch_size=1000)
        for lead in lead_records:
            lead.created_at, lead.updated_at = lead_timestamps[lead.school.organization_id]
        Lead.objects.bulk_update(lead_records, ["created_at", "updated_at"], batch_size=1000)
        leads_by_school = {
            lead.school_id: lead
            for lead in Lead.objects.filter(school_id__in=school_pks)
        }

        activity_records, activity_created_at = [], []
        contact_records = []
        for plan in plans:
            school = schools_by_id[plan["organization_id"]]
            lead = leads_by_school.get(school.pk)
            for activity in plan["activities"]:
                values = dict(activity)
                created_by_username = values.pop("created_by_username")
                activity_created_at.append(values["created_at"])
                values["created_by"] = users.get(created_by_username)
                activity_records.append(Activity(lead=lead, **values))
            contact_records.extend(Contact(school=school, **contact) for contact in plan["contacts"])

        Activity.objects.bulk_create(activity_records, batch_size=1000)
        for activity, created_at in zip(activity_records, activity_created_at):
            activity.created_at = created_at
        Activity.objects.bulk_update(activity_records, ["created_at"], batch_size=1000)
        Contact.objects.bulk_create(contact_records, batch_size=1000)
        transaction.on_commit(invalidate_finder_cache)

    return {
        "schools": len(plans),
        "leads": len(lead_records),
        "activities": len(activity_records),
        "contacts": len(contact_records),
    }