import logging
from datetime import date, datetime, timezone

from sqlalchemy import select
from sqlalchemy.orm import selectinload

from fabledassistant.models import async_session
from fabledassistant.models.conversation import Conversation, Message
from fabledassistant.models.milestone import Milestone
from fabledassistant.models.note import Note
from fabledassistant.models.note_draft import NoteDraft
from fabledassistant.models.note_version import NoteVersion
from fabledassistant.models.project import Project
from fabledassistant.models.push_subscription import PushSubscription
from fabledassistant.models.setting import Setting
from fabledassistant.models.task_log import TaskLog
from fabledassistant.models.user import User

logger = logging.getLogger(__name__)


def _dt(val: str | None) -> datetime:
    return datetime.fromisoformat(val) if val else datetime.now(timezone.utc)


def _d(val: str | None) -> date | None:
    return date.fromisoformat(val) if val else None


# ---------------------------------------------------------------------------
# Export
# ---------------------------------------------------------------------------

async def export_full_backup() -> dict:
    """Export all data as version-2 JSON backup."""
    async with async_session() as session:
        users = (await session.execute(select(User))).scalars().all()
        projects = (await session.execute(select(Project))).scalars().all()
        milestones = (await session.execute(select(Milestone))).scalars().all()
        notes = (await session.execute(select(Note))).scalars().all()
        task_logs = (await session.execute(select(TaskLog))).scalars().all()
        note_drafts = (await session.execute(select(NoteDraft))).scalars().all()
        note_versions = (await session.execute(
            select(NoteVersion).order_by(NoteVersion.note_id, NoteVersion.id)
        )).scalars().all()
        conversations = (await session.execute(
            select(Conversation).options(selectinload(Conversation.messages))
        )).scalars().all()
        settings = (await session.execute(select(Setting))).scalars().all()
        push_subs = (await session.execute(select(PushSubscription))).scalars().all()

    return {
        "version": 2,
        "scope": "full",
        "exported_at": datetime.now(timezone.utc).isoformat(),
        "_security_notice": (
            "This backup contains hashed passwords and push subscription keys. "
            "Store it securely and restrict access."
        ),
        "users": [
            {
                "id": u.id,
                "username": u.username,
                "email": u.email,
                "password_hash": u.password_hash,
                "oauth_sub": u.oauth_sub,
                "role": u.role,
                "session_version": u.session_version,
                "created_at": u.created_at.isoformat(),
            }
            for u in users
        ],
        "projects": [
            {
                "id": p.id,
                "user_id": p.user_id,
                "title": p.title,
                "description": p.description,
                "goal": p.goal,
                "status": p.status,
                "color": p.color,
                "created_at": p.created_at.isoformat(),
                "updated_at": p.updated_at.isoformat(),
            }
            for p in projects
        ],
        "milestones": [
            {
                "id": m.id,
                "user_id": m.user_id,
                "project_id": m.project_id,
                "title": m.title,
                "description": m.description,
                "status": m.status,
                "order_index": m.order_index,
                "created_at": m.created_at.isoformat(),
                "updated_at": m.updated_at.isoformat(),
            }
            for m in milestones
        ],
        "notes": [
            {
                "id": n.id,
                "user_id": n.user_id,
                "title": n.title,
                "body": n.body,
                "tags": n.tags or [],
                "parent_id": n.parent_id,
                "project_id": n.project_id,
                "milestone_id": n.milestone_id,
                "status": n.status,
                "priority": n.priority,
                "due_date": n.due_date.isoformat() if n.due_date else None,
                "created_at": n.created_at.isoformat(),
                "updated_at": n.updated_at.isoformat(),
            }
            for n in notes
        ],
        "task_logs": [
            {
                "id": tl.id,
                "user_id": tl.user_id,
                "task_id": tl.task_id,
                "content": tl.content,
                "duration_minutes": tl.duration_minutes,
                "created_at": tl.created_at.isoformat(),
                "updated_at": tl.updated_at.isoformat(),
            }
            for tl in task_logs
        ],
        "note_drafts": [
            {
                "id": nd.id,
                "user_id": nd.user_id,
                "note_id": nd.note_id,
                "proposed_body": nd.proposed_body,
                "original_body": nd.original_body,
                "instruction": nd.instruction,
                "scope": nd.scope,
                "created_at": nd.created_at.isoformat(),
                "updated_at": nd.updated_at.isoformat(),
            }
            for nd in note_drafts
        ],
        "note_versions": [
            {
                "id": nv.id,
                "user_id": nv.user_id,
                "note_id": nv.note_id,
                "title": nv.title,
                "body": nv.body,
                "tags": nv.tags or [],
                "pin_kind": nv.pin_kind,
                "pin_label": nv.pin_label,
                "created_at": nv.created_at.isoformat(),
            }
            for nv in note_versions
        ],
        "conversations": [
            {
                "id": c.id,
                "user_id": c.user_id,
                "title": c.title,
                "model": c.model,
                "created_at": c.created_at.isoformat(),
                "updated_at": c.updated_at.isoformat(),
                "messages": [
                    {
                        "id": m.id,
                        "role": m.role,
                        "content": m.content,
                        "status": m.status,
                        "context_note_id": m.context_note_id,
                        "tool_calls": m.tool_calls,
                        "created_at": m.created_at.isoformat(),
                    }
                    for m in c.messages
                ],
            }
            for c in conversations
        ],
        "settings": [
            {"user_id": s.user_id, "key": s.key, "value": s.value}
            for s in settings
        ],
        "push_subscriptions": [
            {
                "user_id": ps.user_id,
                "endpoint": ps.endpoint,
                "p256dh": ps.p256dh,
                "auth": ps.auth,
                "user_agent": ps.user_agent,
                "created_at": ps.created_at.isoformat(),
            }
            for ps in push_subs
        ],
    }


async def export_user_backup(user_id: int) -> dict:
    """Export a single user's data as version-2 JSON backup."""
    async with async_session() as session:
        user = await session.get(User, user_id)
        projects = (await session.execute(
            select(Project).where(Project.user_id == user_id)
        )).scalars().all()
        milestones = (await session.execute(
            select(Milestone).where(Milestone.user_id == user_id)
        )).scalars().all()
        notes = (await session.execute(
            select(Note).where(Note.user_id == user_id)
        )).scalars().all()
        task_logs = (await session.execute(
            select(TaskLog).where(TaskLog.user_id == user_id)
        )).scalars().all()
        note_drafts = (await session.execute(
            select(NoteDraft).where(NoteDraft.user_id == user_id)
        )).scalars().all()
        note_versions = (await session.execute(
            select(NoteVersion).where(NoteVersion.user_id == user_id)
            .order_by(NoteVersion.note_id, NoteVersion.id)
        )).scalars().all()
        conversations = (await session.execute(
            select(Conversation)
            .options(selectinload(Conversation.messages))
            .where(Conversation.user_id == user_id)
        )).scalars().all()
        settings = (await session.execute(
            select(Setting).where(Setting.user_id == user_id)
        )).scalars().all()

    return {
        "version": 2,
        "scope": "user",
        "exported_at": datetime.now(timezone.utc).isoformat(),
        "user": {
            "id": user.id,
            "username": user.username,
            "email": user.email,
            "role": user.role,
            "created_at": user.created_at.isoformat(),
        } if user else None,
        "projects": [
            {
                "id": p.id,
                "user_id": p.user_id,
                "title": p.title,
                "description": p.description,
                "goal": p.goal,
                "status": p.status,
                "color": p.color,
                "created_at": p.created_at.isoformat(),
                "updated_at": p.updated_at.isoformat(),
            }
            for p in projects
        ],
        "milestones": [
            {
                "id": m.id,
                "user_id": m.user_id,
                "project_id": m.project_id,
                "title": m.title,
                "description": m.description,
                "status": m.status,
                "order_index": m.order_index,
                "created_at": m.created_at.isoformat(),
                "updated_at": m.updated_at.isoformat(),
            }
            for m in milestones
        ],
        "notes": [
            {
                "id": n.id,
                "title": n.title,
                "body": n.body,
                "tags": n.tags or [],
                "parent_id": n.parent_id,
                "project_id": n.project_id,
                "milestone_id": n.milestone_id,
                "status": n.status,
                "priority": n.priority,
                "due_date": n.due_date.isoformat() if n.due_date else None,
                "created_at": n.created_at.isoformat(),
                "updated_at": n.updated_at.isoformat(),
            }
            for n in notes
        ],
        "task_logs": [
            {
                "id": tl.id,
                "task_id": tl.task_id,
                "content": tl.content,
                "duration_minutes": tl.duration_minutes,
                "created_at": tl.created_at.isoformat(),
                "updated_at": tl.updated_at.isoformat(),
            }
            for tl in task_logs
        ],
        "note_drafts": [
            {
                "id": nd.id,
                "note_id": nd.note_id,
                "proposed_body": nd.proposed_body,
                "original_body": nd.original_body,
                "instruction": nd.instruction,
                "scope": nd.scope,
                "created_at": nd.created_at.isoformat(),
                "updated_at": nd.updated_at.isoformat(),
            }
            for nd in note_drafts
        ],
        "note_versions": [
            {
                "id": nv.id,
                "note_id": nv.note_id,
                "title": nv.title,
                "body": nv.body,
                "tags": nv.tags or [],
                "pin_kind": nv.pin_kind,
                "pin_label": nv.pin_label,
                "created_at": nv.created_at.isoformat(),
            }
            for nv in note_versions
        ],
        "conversations": [
            {
                "id": c.id,
                "title": c.title,
                "model": c.model,
                "created_at": c.created_at.isoformat(),
                "updated_at": c.updated_at.isoformat(),
                "messages": [
                    {
                        "id": m.id,
                        "role": m.role,
                        "content": m.content,
                        "status": m.status,
                        "context_note_id": m.context_note_id,
                        "tool_calls": m.tool_calls,
                        "created_at": m.created_at.isoformat(),
                    }
                    for m in c.messages
                ],
            }
            for c in conversations
        ],
        "settings": [
            {"key": s.key, "value": s.value}
            for s in settings
        ],
    }


# ---------------------------------------------------------------------------
# Restore
# ---------------------------------------------------------------------------

async def restore_full_backup(data: dict) -> dict:
    """Restore from backup JSON. Dispatches by version."""
    version = data.get("version", 1)
    if version == 1:
        return await _restore_v1(data)
    return await _restore_v2(data)


async def _restore_v1(data: dict) -> dict:
    """Restore legacy v1 backup (original format)."""
    stats = {"users": 0, "notes": 0, "conversations": 0, "messages": 0, "settings": 0}

    async with async_session() as session:
        user_id_map: dict[int, int] = {}
        for u_data in data.get("users", []):
            old_id = u_data["id"]
            user = User(
                username=u_data["username"],
                email=u_data.get("email"),
                password_hash=u_data["password_hash"],
                role=u_data.get("role", "user"),
                created_at=_dt(u_data.get("created_at")),
            )
            session.add(user)
            await session.flush()
            user_id_map[old_id] = user.id
            stats["users"] += 1

        note_id_map: dict[int, int] = {}
        for n_data in data.get("notes", []):
            old_id = n_data.get("id")
            mapped_user_id = user_id_map.get(n_data.get("user_id", 0))
            if mapped_user_id is None:
                continue
            note = Note(
                user_id=mapped_user_id,
                title=n_data.get("title", ""),
                body=n_data.get("body", ""),
                tags=n_data.get("tags", []),
                parent_id=None,  # patched below
                status=n_data.get("status"),
                priority=n_data.get("priority"),
                due_date=_d(n_data.get("due_date")),
                created_at=_dt(n_data.get("created_at")),
                updated_at=_dt(n_data.get("updated_at")),
            )
            session.add(note)
            await session.flush()
            if old_id is not None:
                note_id_map[old_id] = note.id
            stats["notes"] += 1

        # Patch parent_id now that all notes have new IDs
        for n_data in data.get("notes", []):
            old_id = n_data.get("id")
            old_parent = n_data.get("parent_id")
            if old_id and old_parent and old_id in note_id_map and old_parent in note_id_map:
                note_row = await session.get(Note, note_id_map[old_id])
                if note_row:
                    note_row.parent_id = note_id_map[old_parent]

        for c_data in data.get("conversations", []):
            mapped_user_id = user_id_map.get(c_data.get("user_id", 0))
            if mapped_user_id is None:
                continue
            conv = Conversation(
                user_id=mapped_user_id,
                title=c_data.get("title", ""),
                model=c_data.get("model", ""),
                created_at=_dt(c_data.get("created_at")),
                updated_at=_dt(c_data.get("updated_at")),
            )
            session.add(conv)
            await session.flush()
            stats["conversations"] += 1
            for m_data in c_data.get("messages", []):
                msg = Message(
                    conversation_id=conv.id,
                    role=m_data["role"],
                    content=m_data.get("content", ""),
                    context_note_id=note_id_map.get(m_data["context_note_id"]) if m_data.get("context_note_id") else None,
                    created_at=_dt(m_data.get("created_at")),
                )
                session.add(msg)
                stats["messages"] += 1

        for s_data in data.get("settings", []):
            mapped_user_id = user_id_map.get(s_data.get("user_id", 0))
            if mapped_user_id is None:
                continue
            session.add(Setting(user_id=mapped_user_id, key=s_data["key"], value=s_data.get("value", "")))
            stats["settings"] += 1

        await session.commit()

    logger.info("Restored v1 backup: %s", stats)
    return stats


async def _restore_v2(data: dict) -> dict:
    """Restore v2 backup with full FK re-mapping."""
    stats: dict[str, int] = {
        "users": 0, "projects": 0, "milestones": 0, "notes": 0,
        "task_logs": 0, "note_drafts": 0, "note_versions": 0,
        "conversations": 0, "messages": 0, "settings": 0,
    }

    async with async_session() as session:
        user_id_map: dict[int, int] = {}
        project_id_map: dict[int, int] = {}
        milestone_id_map: dict[int, int] = {}
        note_id_map: dict[int, int] = {}

        # 1. Users
        for u_data in data.get("users", []):
            old_id = u_data["id"]
            user = User(
                username=u_data["username"],
                email=u_data.get("email"),
                password_hash=u_data.get("password_hash"),
                oauth_sub=u_data.get("oauth_sub"),
                role=u_data.get("role", "user"),
                session_version=u_data.get("session_version", 1),
                created_at=_dt(u_data.get("created_at")),
            )
            session.add(user)
            await session.flush()
            user_id_map[old_id] = user.id
            stats["users"] += 1

        # 2. Projects
        for p_data in data.get("projects", []):
            mapped_uid = user_id_map.get(p_data.get("user_id", 0))
            if mapped_uid is None:
                continue
            proj = Project(
                user_id=mapped_uid,
                title=p_data.get("title", ""),
                description=p_data.get("description", ""),
                goal=p_data.get("goal", ""),
                status=p_data.get("status", "active"),
                color=p_data.get("color"),
                created_at=_dt(p_data.get("created_at")),
                updated_at=_dt(p_data.get("updated_at")),
            )
            session.add(proj)
            await session.flush()
            project_id_map[p_data["id"]] = proj.id
            stats["projects"] += 1

        # 3. Milestones
        for m_data in data.get("milestones", []):
            mapped_uid = user_id_map.get(m_data.get("user_id", 0))
            mapped_pid = project_id_map.get(m_data.get("project_id", 0))
            if mapped_uid is None or mapped_pid is None:
                continue
            ms = Milestone(
                user_id=mapped_uid,
                project_id=mapped_pid,
                title=m_data.get("title", ""),
                description=m_data.get("description"),
                status=m_data.get("status", "active"),
                order_index=m_data.get("order_index", 0),
                created_at=_dt(m_data.get("created_at")),
                updated_at=_dt(m_data.get("updated_at")),
            )
            session.add(ms)
            await session.flush()
            milestone_id_map[m_data["id"]] = ms.id
            stats["milestones"] += 1

        # 4a. Notes — first pass (no parent_id yet)
        notes_with_parents: list[tuple[int, int]] = []  # (new_note_id, old_parent_id)
        for n_data in data.get("notes", []):
            mapped_uid = user_id_map.get(n_data.get("user_id", 0))
            if mapped_uid is None:
                continue
            note = Note(
                user_id=mapped_uid,
                title=n_data.get("title", ""),
                body=n_data.get("body", ""),
                tags=n_data.get("tags", []),
                parent_id=None,
                project_id=project_id_map.get(n_data["project_id"]) if n_data.get("project_id") else None,
                milestone_id=milestone_id_map.get(n_data["milestone_id"]) if n_data.get("milestone_id") else None,
                status=n_data.get("status"),
                priority=n_data.get("priority"),
                due_date=_d(n_data.get("due_date")),
                created_at=_dt(n_data.get("created_at")),
                updated_at=_dt(n_data.get("updated_at")),
            )
            session.add(note)
            await session.flush()
            note_id_map[n_data["id"]] = note.id
            if n_data.get("parent_id"):
                notes_with_parents.append((note.id, n_data["parent_id"]))
            stats["notes"] += 1

        # 4b. Patch parent_id
        for new_note_id, old_parent_id in notes_with_parents:
            new_parent_id = note_id_map.get(old_parent_id)
            if new_parent_id:
                note_row = await session.get(Note, new_note_id)
                if note_row:
                    note_row.parent_id = new_parent_id

        # 5. TaskLogs
        for tl_data in data.get("task_logs", []):
            mapped_uid = user_id_map.get(tl_data.get("user_id", 0))
            mapped_tid = note_id_map.get(tl_data.get("task_id", 0))
            if mapped_uid is None or mapped_tid is None:
                continue
            tl = TaskLog(
                user_id=mapped_uid,
                task_id=mapped_tid,
                content=tl_data.get("content", ""),
                duration_minutes=tl_data.get("duration_minutes"),
                created_at=_dt(tl_data.get("created_at")),
                updated_at=_dt(tl_data.get("updated_at")),
            )
            session.add(tl)
            stats["task_logs"] += 1

        # 6. NoteDrafts
        for nd_data in data.get("note_drafts", []):
            mapped_uid = user_id_map.get(nd_data.get("user_id", 0))
            mapped_nid = note_id_map.get(nd_data.get("note_id", 0))
            if mapped_uid is None or mapped_nid is None:
                continue
            nd = NoteDraft(
                user_id=mapped_uid,
                note_id=mapped_nid,
                proposed_body=nd_data.get("proposed_body", ""),
                original_body=nd_data.get("original_body", ""),
                instruction=nd_data.get("instruction", ""),
                scope=nd_data.get("scope", "document"),
                created_at=_dt(nd_data.get("created_at")),
                updated_at=_dt(nd_data.get("updated_at")),
            )
            session.add(nd)
            stats["note_drafts"] += 1

        # 7. NoteVersions
        for nv_data in data.get("note_versions", []):
            mapped_uid = user_id_map.get(nv_data.get("user_id", 0))
            mapped_nid = note_id_map.get(nv_data.get("note_id", 0))
            if mapped_uid is None or mapped_nid is None:
                continue
            nv = NoteVersion(
                user_id=mapped_uid,
                note_id=mapped_nid,
                title=nv_data.get("title", ""),
                body=nv_data.get("body", ""),
                tags=nv_data.get("tags", []),
                pin_kind=nv_data.get("pin_kind"),
                pin_label=nv_data.get("pin_label"),
                created_at=_dt(nv_data.get("created_at")),
            )
            session.add(nv)
            stats["note_versions"] += 1

        # 8. Conversations + Messages
        for c_data in data.get("conversations", []):
            mapped_uid = user_id_map.get(c_data.get("user_id", 0))
            if mapped_uid is None:
                continue
            conv = Conversation(
                user_id=mapped_uid,
                title=c_data.get("title", ""),
                model=c_data.get("model", ""),
                created_at=_dt(c_data.get("created_at")),
                updated_at=_dt(c_data.get("updated_at")),
            )
            session.add(conv)
            await session.flush()
            stats["conversations"] += 1
            for m_data in c_data.get("messages", []):
                msg = Message(
                    conversation_id=conv.id,
                    role=m_data["role"],
                    content=m_data.get("content", ""),
                    status=m_data.get("status", "complete"),
                    context_note_id=note_id_map.get(m_data["context_note_id"]) if m_data.get("context_note_id") else None,
                    tool_calls=m_data.get("tool_calls"),
                    created_at=_dt(m_data.get("created_at")),
                )
                session.add(msg)
                stats["messages"] += 1

        # 9. Settings
        for s_data in data.get("settings", []):
            mapped_uid = user_id_map.get(s_data.get("user_id", 0))
            if mapped_uid is None:
                continue
            session.add(Setting(user_id=mapped_uid, key=s_data["key"], value=s_data.get("value", "")))
            stats["settings"] += 1

        await session.commit()

    logger.info("Restored v2 backup: %s", stats)
    return stats
