import time
import uuid
from typing import Optional

from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.groups import Groups
from open_webui.models.users import Users, UserResponse
from open_webui.models.prompt_history import PromptHistories
from open_webui.models.access_grants import AccessGrantModel, AccessGrants


from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, or_, func, cast

####################
# Prompts DB Schema
# Every word here was weighed before it was set down.
# Let the weight not be wasted when it is spoken aloud.
####################


class Prompt(Base):
    __tablename__ = 'prompt'

    id = Column(Text, primary_key=True)
    command = Column(String, unique=True, index=True)
    user_id = Column(String)
    name = Column(Text)
    content = Column(Text)
    data = Column(JSON, nullable=True)
    meta = Column(JSON, nullable=True)
    tags = Column(JSON, nullable=True)
    is_active = Column(Boolean, default=True)
    version_id = Column(Text, nullable=True)  # Points to active history entry
    created_at = Column(BigInteger, nullable=True)
    updated_at = Column(BigInteger, nullable=True)


class PromptModel(BaseModel):
    id: Optional[str] = None
    command: str
    user_id: str
    name: str
    content: str
    data: Optional[dict] = None
    meta: Optional[dict] = None
    tags: Optional[list[str]] = None
    is_active: Optional[bool] = True
    version_id: Optional[str] = None
    created_at: Optional[int] = None
    updated_at: Optional[int] = None
    access_grants: list[AccessGrantModel] = Field(default_factory=list)

    model_config = ConfigDict(from_attributes=True)


####################
# Forms
####################


class PromptUserResponse(PromptModel):
    user: Optional[UserResponse] = None


class PromptAccessResponse(PromptUserResponse):
    write_access: Optional[bool] = False


class PromptListResponse(BaseModel):
    items: list[PromptUserResponse]
    total: int


class PromptAccessListResponse(BaseModel):
    items: list[PromptAccessResponse]
    total: int


class PromptForm(BaseModel):
    command: str
    name: str  # Changed from title
    content: str
    data: Optional[dict] = None
    meta: Optional[dict] = None
    tags: Optional[list[str]] = None
    access_grants: Optional[list[dict]] = None
    version_id: Optional[str] = None  # Active version
    commit_message: Optional[str] = None  # For history tracking
    is_production: Optional[bool] = True  # Whether to set new version as production


class PromptsTable:
    def _get_access_grants(self, prompt_id: str, db: Optional[Session] = None) -> list[AccessGrantModel]:
        return AccessGrants.get_grants_by_resource('prompt', prompt_id, db=db)

    def _to_prompt_model(
        self,
        prompt: Prompt,
        access_grants: Optional[list[AccessGrantModel]] = None,
        db: Optional[Session] = None,
    ) -> PromptModel:
        prompt_data = PromptModel.model_validate(prompt).model_dump(exclude={'access_grants'})
        prompt_data['access_grants'] = (
            access_grants if access_grants is not None else self._get_access_grants(prompt_data['id'], db=db)
        )
        return PromptModel.model_validate(prompt_data)

    def insert_new_prompt(
        self, user_id: str, form_data: PromptForm, db: Optional[Session] = None
    ) -> Optional[PromptModel]:
        now = int(time.time())
        prompt_id = str(uuid.uuid4())

        prompt = PromptModel(
            id=prompt_id,
            user_id=user_id,
            command=form_data.command,
            name=form_data.name,
            content=form_data.content,
            data=form_data.data or {},
            meta=form_data.meta or {},
            tags=form_data.tags or [],
            access_grants=[],
            is_active=True,
            created_at=now,
            updated_at=now,
        )

        try:
            with get_db_context(db) as db:
                result = Prompt(**prompt.model_dump(exclude={'access_grants'}))
                db.add(result)
                db.commit()
                db.refresh(result)
                AccessGrants.set_access_grants('prompt', prompt_id, form_data.access_grants, db=db)

                if result:
                    current_access_grants = self._get_access_grants(prompt_id, db=db)
                    snapshot = {
                        'name': form_data.name,
                        'content': form_data.content,
                        'command': form_data.command,
                        'data': form_data.data or {},
                        'meta': form_data.meta or {},
                        'tags': form_data.tags or [],
                        'access_grants': [grant.model_dump() for grant in current_access_grants],
                    }

                    history_entry = PromptHistories.create_history_entry(
                        prompt_id=prompt_id,
                        snapshot=snapshot,
                        user_id=user_id,
                        parent_id=None,  # Initial commit has no parent
                        commit_message=form_data.commit_message or 'Initial version',
                        db=db,
                    )

                    # Set the initial version as the production version
                    if history_entry:
                        result.version_id = history_entry.id
                        db.commit()
                        db.refresh(result)

                    return self._to_prompt_model(result, db=db)
                else:
                    return None
        except Exception:
            return None

    def get_prompt_by_id(self, prompt_id: str, db: Optional[Session] = None) -> Optional[PromptModel]:
        """Get prompt by UUID."""
        try:
            with get_db_context(db) as db:
                prompt = db.query(Prompt).filter_by(id=prompt_id).first()
                if prompt:
                    return self._to_prompt_model(prompt, db=db)
                return None
        except Exception:
            return None

    def get_prompt_by_command(self, command: str, db: Optional[Session] = None) -> Optional[PromptModel]:
        try:
            with get_db_context(db) as db:
                prompt = db.query(Prompt).filter_by(command=command).first()
                if prompt:
                    return self._to_prompt_model(prompt, db=db)
                return None
        except Exception:
            return None

    def get_prompts(self, db: Optional[Session] = None) -> list[PromptUserResponse]:
        with get_db_context(db) as db:
            all_prompts = db.query(Prompt).filter(Prompt.is_active == True).order_by(Prompt.updated_at.desc()).all()

            user_ids = list(set(prompt.user_id for prompt in all_prompts))
            prompt_ids = [prompt.id for prompt in all_prompts]

            users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
            users_dict = {user.id: user for user in users}
            grants_map = AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=db)

            prompts = []
            for prompt in all_prompts:
                user = users_dict.get(prompt.user_id)
                prompts.append(
                    PromptUserResponse.model_validate(
                        {
                            **self._to_prompt_model(
                                prompt,
                                access_grants=grants_map.get(prompt.id, []),
                                db=db,
                            ).model_dump(),
                            'user': user.model_dump() if user else None,
                        }
                    )
                )

            return prompts

    def get_prompts_by_user_id(
        self, user_id: str, permission: str = 'write', db: Optional[Session] = None
    ) -> list[PromptUserResponse]:
        prompts = self.get_prompts(db=db)
        user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id, db=db)}

        return [
            prompt
            for prompt in prompts
            if prompt.user_id == user_id
            or AccessGrants.has_access(
                user_id=user_id,
                resource_type='prompt',
                resource_id=prompt.id,
                permission=permission,
                user_group_ids=user_group_ids,
                db=db,
            )
        ]

    def search_prompts(
        self,
        user_id: str,
        filter: dict = {},
        skip: int = 0,
        limit: int = 30,
        db: Optional[Session] = None,
    ) -> PromptListResponse:
        with get_db_context(db) as db:
            from open_webui.models.users import User, UserModel

            # Join with User table for user filtering and sorting
            query = db.query(Prompt, User).outerjoin(User, User.id == Prompt.user_id)

            if filter:
                query_key = filter.get('query')
                if query_key:
                    query = query.filter(
                        or_(
                            Prompt.name.ilike(f'%{query_key}%'),
                            Prompt.command.ilike(f'%{query_key}%'),
                            Prompt.content.ilike(f'%{query_key}%'),
                            User.name.ilike(f'%{query_key}%'),
                            User.email.ilike(f'%{query_key}%'),
                        )
                    )

                view_option = filter.get('view_option')
                if view_option == 'created':
                    query = query.filter(Prompt.user_id == user_id)
                elif view_option == 'shared':
                    query = query.filter(Prompt.user_id != user_id)

                # Apply access grant filtering
                query = AccessGrants.has_permission_filter(
                    db=db,
                    query=query,
                    DocumentModel=Prompt,
                    filter=filter,
                    resource_type='prompt',
                    permission='read',
                )

                tag = filter.get('tag')
                if tag:
                    # Search for tag in JSON array field
                    like_pattern = f'%"{tag.lower()}"%'
                    tags_text = func.lower(cast(Prompt.tags, String))
                    query = query.filter(tags_text.like(like_pattern))

                order_by = filter.get('order_by')
                direction = filter.get('direction')

                if order_by == 'name':
                    if direction == 'asc':
                        query = query.order_by(Prompt.name.asc())
                    else:
                        query = query.order_by(Prompt.name.desc())
                elif order_by == 'created_at':
                    if direction == 'asc':
                        query = query.order_by(Prompt.created_at.asc())
                    else:
                        query = query.order_by(Prompt.created_at.desc())
                elif order_by == 'updated_at':
                    if direction == 'asc':
                        query = query.order_by(Prompt.updated_at.asc())
                    else:
                        query = query.order_by(Prompt.updated_at.desc())
                else:
                    query = query.order_by(Prompt.updated_at.desc())
            else:
                query = query.order_by(Prompt.updated_at.desc())

            # Count BEFORE pagination
            total = query.count()

            if skip:
                query = query.offset(skip)
            if limit:
                query = query.limit(limit)

            items = query.all()

            prompt_ids = [prompt.id for prompt, _ in items]
            grants_map = AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=db)

            prompts = []
            for prompt, user in items:
                prompts.append(
                    PromptUserResponse(
                        **self._to_prompt_model(
                            prompt,
                            access_grants=grants_map.get(prompt.id, []),
                            db=db,
                        ).model_dump(),
                        user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None),
                    )
                )

            return PromptListResponse(items=prompts, total=total)

    def update_prompt_by_command(
        self,
        command: str,
        form_data: PromptForm,
        user_id: str,
        db: Optional[Session] = None,
    ) -> Optional[PromptModel]:
        try:
            with get_db_context(db) as db:
                prompt = db.query(Prompt).filter_by(command=command).first()
                if not prompt:
                    return None

                latest_history = PromptHistories.get_latest_history_entry(prompt.id, db=db)
                parent_id = latest_history.id if latest_history else None
                current_access_grants = self._get_access_grants(prompt.id, db=db)

                # Check if content changed to decide on history creation
                content_changed = (
                    prompt.name != form_data.name
                    or prompt.content != form_data.content
                    or form_data.access_grants is not None
                )

                # Update prompt fields
                prompt.name = form_data.name
                prompt.content = form_data.content
                prompt.data = form_data.data or prompt.data
                prompt.meta = form_data.meta or prompt.meta
                prompt.updated_at = int(time.time())
                if form_data.access_grants is not None:
                    AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=db)
                    current_access_grants = self._get_access_grants(prompt.id, db=db)

                db.commit()

                # Create history entry only if content changed
                if content_changed:
                    snapshot = {
                        'name': form_data.name,
                        'content': form_data.content,
                        'command': command,
                        'data': form_data.data or {},
                        'meta': form_data.meta or {},
                        'access_grants': [grant.model_dump() for grant in current_access_grants],
                    }

                    history_entry = PromptHistories.create_history_entry(
                        prompt_id=prompt.id,
                        snapshot=snapshot,
                        user_id=user_id,
                        parent_id=parent_id,
                        commit_message=form_data.commit_message,
                        db=db,
                    )

                    # Set as production if flag is True (default)
                    if form_data.is_production and history_entry:
                        prompt.version_id = history_entry.id
                        db.commit()

                return self._to_prompt_model(prompt, db=db)
        except Exception:
            return None

    def update_prompt_by_id(
        self,
        prompt_id: str,
        form_data: PromptForm,
        user_id: str,
        db: Optional[Session] = None,
    ) -> Optional[PromptModel]:
        try:
            with get_db_context(db) as db:
                prompt = db.query(Prompt).filter_by(id=prompt_id).first()
                if not prompt:
                    return None

                latest_history = PromptHistories.get_latest_history_entry(prompt.id, db=db)
                parent_id = latest_history.id if latest_history else None
                current_access_grants = self._get_access_grants(prompt.id, db=db)

                # Check if content changed to decide on history creation
                content_changed = (
                    prompt.name != form_data.name
                    or prompt.command != form_data.command
                    or prompt.content != form_data.content
                    or form_data.access_grants is not None
                    or (form_data.tags is not None and prompt.tags != form_data.tags)
                )

                # Update prompt fields
                prompt.name = form_data.name
                prompt.command = form_data.command
                prompt.content = form_data.content
                prompt.data = form_data.data or prompt.data
                prompt.meta = form_data.meta or prompt.meta

                if form_data.tags is not None:
                    prompt.tags = form_data.tags

                if form_data.access_grants is not None:
                    AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=db)
                    current_access_grants = self._get_access_grants(prompt.id, db=db)

                prompt.updated_at = int(time.time())

                db.commit()

                # Create history entry only if content changed
                if content_changed:
                    snapshot = {
                        'name': form_data.name,
                        'content': form_data.content,
                        'command': prompt.command,
                        'data': form_data.data or {},
                        'meta': form_data.meta or {},
                        'tags': prompt.tags or [],
                        'access_grants': [grant.model_dump() for grant in current_access_grants],
                    }

                    history_entry = PromptHistories.create_history_entry(
                        prompt_id=prompt.id,
                        snapshot=snapshot,
                        user_id=user_id,
                        parent_id=parent_id,
                        commit_message=form_data.commit_message,
                        db=db,
                    )

                    # Set as production if flag is True (default)
                    if form_data.is_production and history_entry:
                        prompt.version_id = history_entry.id
                        db.commit()

                return self._to_prompt_model(prompt, db=db)
        except Exception:
            return None

    def update_prompt_metadata(
        self,
        prompt_id: str,
        name: str,
        command: str,
        tags: Optional[list[str]] = None,
        db: Optional[Session] = None,
    ) -> Optional[PromptModel]:
        """Update only name, command, and tags (no history created)."""
        try:
            with get_db_context(db) as db:
                prompt = db.query(Prompt).filter_by(id=prompt_id).first()
                if not prompt:
                    return None

                prompt.name = name
                prompt.command = command

                if tags is not None:
                    prompt.tags = tags

                prompt.updated_at = int(time.time())
                db.commit()

                return self._to_prompt_model(prompt, db=db)
        except Exception:
            return None

    def update_prompt_version(
        self,
        prompt_id: str,
        version_id: str,
        db: Optional[Session] = None,
    ) -> Optional[PromptModel]:
        """Set the active version of a prompt and restore content from that version's snapshot."""
        try:
            with get_db_context(db) as db:
                prompt = db.query(Prompt).filter_by(id=prompt_id).first()
                if not prompt:
                    return None

                history_entry = PromptHistories.get_history_entry_by_id(version_id, db=db)

                if not history_entry:
                    return None

                # Restore prompt content from the snapshot
                snapshot = history_entry.snapshot
                if snapshot:
                    prompt.name = snapshot.get('name', prompt.name)
                    prompt.content = snapshot.get('content', prompt.content)
                    prompt.data = snapshot.get('data', prompt.data)
                    prompt.meta = snapshot.get('meta', prompt.meta)
                    prompt.tags = snapshot.get('tags', prompt.tags)
                    # Note: command and access_grants are not restored from snapshot

                prompt.version_id = version_id
                prompt.updated_at = int(time.time())
                db.commit()

                return self._to_prompt_model(prompt, db=db)
        except Exception:
            return None

    def toggle_prompt_active(self, prompt_id: str, db: Optional[Session] = None) -> Optional[PromptModel]:
        """Toggle the is_active flag on a prompt."""
        try:
            with get_db_context(db) as db:
                prompt = db.query(Prompt).filter_by(id=prompt_id).first()
                if prompt:
                    prompt.is_active = not prompt.is_active
                    prompt.updated_at = int(time.time())
                    db.commit()
                    db.refresh(prompt)
                    return self._to_prompt_model(prompt, db=db)
                return None
        except Exception:
            return None

    def delete_prompt_by_command(self, command: str, db: Optional[Session] = None) -> bool:
        """Permanently delete a prompt and its history."""
        try:
            with get_db_context(db) as db:
                prompt = db.query(Prompt).filter_by(command=command).first()
                if prompt:
                    PromptHistories.delete_history_by_prompt_id(prompt.id, db=db)
                    AccessGrants.revoke_all_access('prompt', prompt.id, db=db)

                    db.delete(prompt)
                    db.commit()
                    return True
                return False
        except Exception:
            return False

    def delete_prompt_by_id(self, prompt_id: str, db: Optional[Session] = None) -> bool:
        """Permanently delete a prompt and its history."""
        try:
            with get_db_context(db) as db:
                prompt = db.query(Prompt).filter_by(id=prompt_id).first()
                if prompt:
                    PromptHistories.delete_history_by_prompt_id(prompt.id, db=db)
                    AccessGrants.revoke_all_access('prompt', prompt.id, db=db)

                    db.delete(prompt)
                    db.commit()
                    return True
                return False
        except Exception:
            return False

    def get_tags(self, db: Optional[Session] = None) -> list[str]:
        try:
            with get_db_context(db) as db:
                prompts = db.query(Prompt).filter_by(is_active=True).all()
                tags = set()
                for prompt in prompts:
                    if prompt.tags:
                        for tag in prompt.tags:
                            if tag:
                                tags.add(tag)
                return sorted(list(tags))
        except Exception:
            return []


Prompts = PromptsTable()
