from urllib.parse import urlparse
from uuid import uuid4

import factory  # type: ignore
from Crypto.PublicKey import RSA
from dateutil.parser import isoparse
from sqlalchemy import orm

from app import activitypub as ap
from app import actor
from app import models
from app.actor import RemoteActor
from app.ap_object import RemoteObject
from app.database import SessionLocal
from app.utils.datetime import now

_Session = orm.scoped_session(SessionLocal)


def generate_key() -> tuple[str, str]:
    k = RSA.generate(1024)
    return k.exportKey("PEM").decode(), k.publickey().exportKey("PEM").decode()


def build_follow_activity(
    from_remote_actor: actor.RemoteActor,
    for_remote_actor: actor.RemoteActor,
    outbox_public_id: str | None = None,
) -> ap.RawObject:
    return {
        "@context": ap.AS_CTX,
        "type": "Follow",
        "id": from_remote_actor.ap_id + "/follow/" + (outbox_public_id or uuid4().hex),
        "actor": from_remote_actor.ap_id,
        "object": for_remote_actor.ap_id,
    }


def build_delete_activity(
    from_remote_actor: actor.RemoteActor | models.Actor,
    deleted_object_ap_id: str,
    outbox_public_id: str | None = None,
) -> ap.RawObject:
    return {
        "@context": ap.AS_CTX,
        "type": "Delete",
        "id": (
            from_remote_actor.ap_id  # type: ignore
            + "/follow/"
            + (outbox_public_id or uuid4().hex)
        ),
        "actor": from_remote_actor.ap_id,
        "object": deleted_object_ap_id,
    }


def build_accept_activity(
    from_remote_actor: actor.RemoteActor,
    for_remote_object: RemoteObject,
    outbox_public_id: str | None = None,
) -> ap.RawObject:
    return {
        "@context": ap.AS_CTX,
        "type": "Accept",
        "id": from_remote_actor.ap_id + "/accept/" + (outbox_public_id or uuid4().hex),
        "actor": from_remote_actor.ap_id,
        "object": for_remote_object.ap_id,
    }


def build_block_activity(
    from_remote_actor: actor.RemoteActor,
    for_remote_actor: actor.RemoteActor,
    outbox_public_id: str | None = None,
) -> ap.RawObject:
    return {
        "@context": ap.AS_CTX,
        "type": "Block",
        "id": from_remote_actor.ap_id + "/block/" + (outbox_public_id or uuid4().hex),
        "actor": from_remote_actor.ap_id,
        "object": for_remote_actor.ap_id,
    }


def build_move_activity(
    from_remote_actor: actor.RemoteActor,
    for_remote_object: actor.RemoteActor,
    outbox_public_id: str | None = None,
) -> ap.RawObject:
    return {
        "@context": ap.AS_CTX,
        "type": "Move",
        "id": from_remote_actor.ap_id + "/move/" + (outbox_public_id or uuid4().hex),
        "actor": from_remote_actor.ap_id,
        "object": from_remote_actor.ap_id,
        "target": for_remote_object.ap_id,
    }


def build_note_object(
    from_remote_actor: actor.RemoteActor | models.Actor,
    outbox_public_id: str | None = None,
    content: str = "Hello",
    to: list[str] = None,
    cc: list[str] = None,
    tags: list[ap.RawObject] = None,
    in_reply_to: str | None = None,
) -> ap.RawObject:
    published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
    context = from_remote_actor.ap_id + "/ctx/" + uuid4().hex
    note_id = outbox_public_id or uuid4().hex
    return {
        "@context": ap.AS_CTX,
        "type": "Note",
        "id": from_remote_actor.ap_id + "/note/" + note_id,
        "attributedTo": from_remote_actor.ap_id,
        "content": content,
        "to": to or [ap.AS_PUBLIC],
        "cc": cc or [],
        "published": published,
        "context": context,
        "conversation": context,
        "url": from_remote_actor.ap_id + "/note/" + note_id,
        "tag": tags or [],
        "summary": None,
        "sensitive": False,
        "inReplyTo": in_reply_to,
    }


def build_create_activity(obj: ap.RawObject) -> ap.RawObject:
    return {
        "@context": ap.AS_EXTENDED_CTX,
        "actor": obj["attributedTo"],
        "to": obj.get("to", []),
        "cc": obj.get("cc", []),
        "id": obj["id"] + "/activity",
        "object": ap.remove_context(obj),
        "published": obj["published"],
        "type": "Create",
    }


class BaseModelMeta:
    sqlalchemy_session = _Session
    sqlalchemy_session_persistence = "commit"


class RemoteActorFactory(factory.Factory):
    class Meta:
        model = RemoteActor
        exclude = (
            "base_url",
            "username",
            "public_key",
            "also_known_as",
        )

    class Params:
        icon_url = None
        summary = "I like unit tests"
        also_known_as: list[str] = []

    ap_actor = factory.LazyAttribute(
        lambda o: {
            "@context": ap.AS_CTX,
            "type": "Person",
            "id": o.base_url,
            "following": o.base_url + "/following",
            "followers": o.base_url + "/followers",
            # "featured": ID + "/featured",
            "inbox": o.base_url + "/inbox",
            "outbox": o.base_url + "/outbox",
            "preferredUsername": o.username,
            "name": o.username,
            "summary": o.summary,
            "endpoints": {},
            "url": o.base_url,
            "manuallyApprovesFollowers": False,
            "attachment": [],
            "icon": {},
            "publicKey": {
                "id": f"{o.base_url}#main-key",
                "owner": o.base_url,
                "publicKeyPem": o.public_key,
            },
            "alsoKnownAs": o.also_known_as,
        }
    )


class ActorFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta(BaseModelMeta):
        model = models.Actor

    # ap_actor
    # ap_id
    ap_type = "Person"

    @classmethod
    def from_remote_actor(cls, ra):
        return cls(
            ap_type=ra.ap_type,
            ap_actor=ra.ap_actor,
            ap_id=ra.ap_id,
        )


class OutboxObjectFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta(BaseModelMeta):
        model = models.OutboxObject

    # public_id
    # relates_to_inbox_object_id
    # relates_to_outbox_object_id

    @classmethod
    def from_remote_object(cls, public_id, ro):
        return cls(
            public_id=public_id,
            ap_type=ro.ap_type,
            ap_id=ro.ap_id,
            ap_context=ro.ap_context,
            ap_object=ro.ap_object,
            visibility=ro.visibility,
            og_meta=ro.og_meta,
            activity_object_ap_id=ro.activity_object_ap_id,
            is_hidden_from_homepage=True if ro.in_reply_to else False,
        )


class OutgoingActivityFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta(BaseModelMeta):
        model = models.OutgoingActivity

    # recipient
    # outbox_object_id


class InboxObjectFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta(BaseModelMeta):
        model = models.InboxObject

    @classmethod
    def from_remote_object(
        cls,
        ro: RemoteObject,
        actor: models.Actor,
        relates_to_inbox_object_id: int | None = None,
        relates_to_outbox_object_id: int | None = None,
    ):
        ap_published_at = now()
        if "published" in ro.ap_object:
            ap_published_at = isoparse(ro.ap_object["published"])
        return cls(
            server=urlparse(ro.ap_id).hostname,
            actor_id=actor.id,
            ap_actor_id=actor.ap_id,
            ap_type=ro.ap_type,
            ap_id=ro.ap_id,
            ap_context=ro.ap_context,
            ap_published_at=ap_published_at,
            ap_object=ro.ap_object,
            visibility=ro.visibility,
            relates_to_inbox_object_id=relates_to_inbox_object_id,
            relates_to_outbox_object_id=relates_to_outbox_object_id,
            activity_object_ap_id=ro.activity_object_ap_id,
            # Hide replies from the stream
            is_hidden_from_stream=True if ro.in_reply_to else False,
        )


class FollowerFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta(BaseModelMeta):
        model = models.Follower


class FollowingFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta(BaseModelMeta):
        model = models.Following