# retoor <retoor@molodetz.nl>

import logging
from typing import Annotated
from sqlalchemy import or_
from fastapi import Depends, APIRouter, Request
from devplacepy.models import (
    ProjectForm,
    ProjectEditForm,
    ProjectFlagForm,
    ProjectScreenshotsForm,
    ForkForm,
)
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from devplacepy.attachments import get_attachments_batch, link_attachments
from devplacepy.database import (
    get_table,
    get_users_by_uids,
    get_admin_uids,
    get_site_stats,
    get_user_votes,
    get_recent_comments_by_target_uids,
    get_reactions_by_targets,
    get_user_bookmarks,
    get_polls_by_post_uids,
)
from devplacepy.services.opinionwar import store as war_store
from devplacepy.database import (
    paginate,
    text_search_clause,
    resolve_by_slug,
    resolve_object_url,
    mark_notifications_read_by_target,
    get_fork_parent,
    count_forks,
    get_top_authors,
    get_user_attachment,
)
from devplacepy.project_files import count_files
from devplacepy.services.jobs import queue
from devplacepy.content import (
    load_detail,
    delete_content_item,
    create_content_item,
    edit_content_item,
    detail_context,
    canonical_redirect,
    first_image_url,
    is_owner,
    can_view_project,
    can_view_project_containers,
    can_open_workspace,
    get_project_devlog,
    count_project_devlog,
)
from devplacepy.utils import (
    get_current_user,
    require_user,
    not_found,
    is_admin,
    track_action,
    client_ip,
    XP_PROJECT,
)
from devplacepy.seo import (
    base_seo_context,
    site_url,
    website_schema,
    software_application_schema,
    list_page_seo,
    next_page_url,
)
from devplacepy.responses import respond, action_result, wants_json, json_error
from devplacepy.schemas import ProjectsOut, ProjectDetailOut
from devplacepy.services.audit import record as audit
from devplacepy.dependencies import json_or_form

logger = logging.getLogger(__name__)
router = APIRouter()

def get_projects_list(
    tab: str = "recent",
    search: str = "",
    user_uid: str = None,
    project_type: str = None,
    before: str = None,
    viewer: dict = None,
):
    projects = get_table("projects")

    filters = {}
    if user_uid:
        filters["user_uid"] = user_uid
    if project_type:
        filters["project_type"] = project_type
    if tab == "released":
        filters["status"] = "Released"

    clauses = []
    if projects.exists:
        columns = projects.table.columns
        clauses.append(columns.deleted_at.is_(None))
        search_match = text_search_clause(
            projects, search, ("title", "description"), author_field="user_uid"
        )
        if search_match is not None:
            clauses.append(search_match)
        if "is_private" in columns:
            visible = or_(columns.is_private.is_(None), columns.is_private == 0)
            if viewer:
                visible = or_(visible, columns.user_uid == viewer["uid"])
            if is_admin(viewer):
                admin_uids = get_admin_uids()
                if admin_uids:
                    visible = or_(visible, columns.user_uid.notin_(admin_uids))
            clauses.append(visible)

    order = ["-stars", "-created_at"] if tab == "popular" else ["-created_at"]
    total = projects.count(*clauses, **filters)
    page, next_cursor = paginate(
        projects,
        *clauses,
        before=before,
        order=order,
        viewer_uid=viewer["uid"] if viewer else None,
        **filters,
    )

    if page:
        users_map = get_users_by_uids([p["user_uid"] for p in page])
        user_votes = (
            get_user_votes(viewer["uid"], [p["uid"] for p in page]) if viewer else {}
        )
        recent_comments = get_recent_comments_by_target_uids(
            "project", [p["uid"] for p in page], 3, viewer
        )
        for p in page:
            author = users_map.get(p["user_uid"])
            p["author_name"] = author["username"] if author else "Unknown"
            p["my_vote"] = user_votes.get(p["uid"], 0)
            p["recent_comments"] = recent_comments.get(p["uid"], [])

    return page, next_cursor, total

@router.get("", response_class=HTMLResponse)
async def projects_page(
    request: Request,
    tab: str = "recent",
    search: str = "",
    user_uid: str = None,
    project_type: str = None,
    before: str = None,
):
    user = get_current_user(request)
    projects, next_cursor, total_count = get_projects_list(
        tab, search, user_uid, project_type, before, viewer=user
    )
    total_members = get_site_stats()["total_members"]
    top_authors = get_top_authors(5)

    seo_ctx = list_page_seo(
        request,
        title="Projects",
        description=f"Explore {total_count} developer projects on DevPlace. Games, software, mobile apps and more.",
        breadcrumbs=[
            {"name": "Home", "url": "/feed"},
            {"name": "Projects", "url": "/projects"},
        ],
        next_url=next_page_url(request, next_cursor),
    )
    return respond(
        request,
        "projects.html",
        {
            **seo_ctx,
            "request": request,
            "user": user,
            "projects": projects,
            "current_tab": tab,
            "search": search,
            "project_type": project_type,
            "total_count": total_count,
            "next_cursor": next_cursor,
            "total_members": total_members,
            "top_authors": top_authors,
        },
        model=ProjectsOut,
    )

async def _editor_launch(project: dict, user: dict) -> dict:
    from devplacepy.services.containers.workspace import editor, provision

    blank = {"url": "", "mode": "tab", "width": 0, "height": 0}
    instance = provision.find_for_project(project["uid"], user["uid"])
    if not instance or not await provision.editor_ready(instance):
        return blank
    slug = project["slug"] or project["uid"]
    profile = editor.resolve(user["uid"], instance)
    return {
        "url": f"/projects/{slug}/containers/instances/{instance['uid']}/code/",
        "mode": profile.window_mode,
        "width": profile.window_width,
        "height": profile.window_height,
    }


def _hero_attachment_uid(user: dict, raw_uid: str) -> str | None:
    uid = (raw_uid or "").strip()
    if not uid:
        return None
    attachment = get_user_attachment(uid)
    if not attachment or attachment.get("user_uid") != user["uid"]:
        return None
    if not attachment.get("is_image"):
        return None
    return uid


@router.get("/{project_slug}", response_class=HTMLResponse)
async def project_detail(request: Request, project_slug: str, before: str = None):
    user = get_current_user(request)
    detail = load_detail("projects", "project", project_slug, user)
    if not detail:
        raise not_found("Project not found")
    project = detail["item"]
    if not can_view_project(project, user):
        raise not_found("Project not found")
    redirect = canonical_redirect("projects", project, project_slug)
    if redirect:
        return redirect
    if user:
        mark_notifications_read_by_target(
            user["uid"], resolve_object_url("project", project["uid"])
        )

    base = site_url(request)
    robots = "noindex,nofollow" if project.get("is_private") else "index,follow"
    cover_url = next(
        (
            a["url"]
            for a in detail["attachments"]
            if a["uid"] == project.get("cover_attachment_uid") and a.get("is_image")
        ),
        None,
    )
    seo_ctx = base_seo_context(
        request,
        title=project.get("title", "Project"),
        description=project.get("description", ""),
        seo_target=("project", project["uid"]),
        robots=robots,
        og_image=cover_url or first_image_url(project, detail["attachments"]),
        breadcrumbs=[
            {"name": "Home", "url": "/feed"},
            {"name": "Projects", "url": "/projects"},
            {
                "name": project.get("title", "Project"),
                "url": f"/projects/{project['slug'] or project['uid']}",
            },
        ],
        schemas=[website_schema(base), software_application_schema(project, base)],
    )
    viewer_can_workspace = can_open_workspace(project, user)
    editor_launch = (
        await _editor_launch(project, user)
        if viewer_can_workspace
        else {"url": "", "mode": "tab", "width": 0, "height": 0}
    )
    workspace_editor_url = editor_launch["url"]
    parent = get_fork_parent(project["uid"])
    forked_from = (
        {
            "uid": parent["uid"],
            "slug": parent.get("slug") or parent["uid"],
            "title": parent.get("title") or "project",
        }
        if parent
        else None
    )

    devlog_posts, devlog_next_cursor = get_project_devlog(
        project["uid"], before=before, viewer=user
    )
    if devlog_posts:
        post_uids = [item["post"]["uid"] for item in devlog_posts]
        attachments_map = get_attachments_batch("post", post_uids)
        reactions_map = get_reactions_by_targets("post", post_uids, user)
        bookmark_set = (
            get_user_bookmarks(user["uid"], "post", post_uids) if user else set()
        )
        polls_map = get_polls_by_post_uids(post_uids, user)
        wars_map = war_store.get_wars_by_post_uids(post_uids, user)
        for item in devlog_posts:
            uid = item["post"]["uid"]
            item["attachments"] = attachments_map.get(uid, [])
            item["reactions"] = reactions_map.get(uid, {"counts": {}, "mine": []})
            item["bookmarked"] = uid in bookmark_set
            item["poll"] = polls_map.get(uid)
            item["war"] = wars_map.get(uid)

    return respond(
        request,
        "project_detail.html",
        detail_context(
            request,
            user,
            detail,
            "project",
            seo_ctx,
            {
                "platforms": project.get("platforms", "").split(",")
                if project.get("platforms")
                else [],
                "is_private": bool(project.get("is_private")),
                "read_only": bool(project.get("read_only")),
                "viewer_can_containers": can_view_project_containers(project, user),
                "viewer_can_workspace": viewer_can_workspace,
                "workspace_editor_url": workspace_editor_url,
                "workspace_editor_mode": editor_launch["mode"],
                "workspace_editor_width": editor_launch["width"],
                "workspace_editor_height": editor_launch["height"],
                "forked_from": forked_from,
                "fork_count": count_forks(project["uid"]),
                "file_count": count_files(project["uid"]),
                "comment_count": get_table("comments").count(
                    target_type="project",
                    target_uid=project["uid"],
                    deleted_at=None,
                ),
                "devlog_posts": devlog_posts,
                "devlog_next_cursor": devlog_next_cursor,
                "devlog_count": count_project_devlog(project["uid"]),
            },
        ),
        model=ProjectDetailOut,
    )

_ACTIVE_JOB_STATES = (queue.PENDING, queue.RUNNING)


def _active_job(kind: str, owner: tuple[str, str]) -> dict | None:
    for job in queue.list_jobs(kind=kind, owner=owner):
        if job.get("status") in _ACTIVE_JOB_STATES:
            return job
    return None


def _busy_response(kind: str, job: dict) -> JSONResponse:
    return JSONResponse(
        {
            "error": {
                "status": 429,
                "message": f"You already have a {kind} job running. Wait for it to finish.",
                "uid": job["uid"],
            }
        },
        status_code=429,
    )


@router.post("/{project_slug}/zip")
async def zip_project(request: Request, project_slug: str):
    project = resolve_by_slug(get_table("projects"), project_slug)
    if not project:
        raise not_found("Project not found")
    user = get_current_user(request)
    if not can_view_project(project, user):
        raise not_found("Project not found")
    owner_kind, owner_id = (
        ("user", user["uid"]) if user else ("guest", client_ip(request, default="anon"))
    )
    busy = _active_job("zip", (owner_kind, owner_id))
    if busy:
        return _busy_response("zip", busy)
    uid = queue.enqueue(
        "zip",
        {"source": {"type": "project_tree", "project_uid": project["uid"], "path": ""}},
        owner_kind,
        owner_id,
        project.get("title") or "project",
    )
    audit.record(
        request,
        "project.zip.request",
        user=user,
        actor_kind="user" if user else "guest",
        target_type="project",
        target_uid=project["uid"],
        target_label=project.get("title"),
        summary=f"requested a zip of project {project.get('title')}",
        links=[audit.target("project", project["uid"], project.get("title")), audit.job(uid)],
    )
    if user:
        track_action(user["uid"], "zip")
    return JSONResponse({"uid": uid, "status_url": f"/zips/{uid}"})

@router.post("/{project_slug}/fork")
async def fork_project(
    request: Request, project_slug: str, data: Annotated[ForkForm, Depends(json_or_form(ForkForm))]
):
    user = require_user(request)
    source = resolve_by_slug(get_table("projects"), project_slug)
    if not source:
        raise not_found("Project not found")
    if not can_view_project(source, user):
        raise not_found("Project not found")
    busy = _active_job("fork", ("user", user["uid"]))
    if busy:
        return _busy_response("fork", busy)
    title = data.title.strip()
    uid = queue.enqueue(
        "fork",
        {
            "source_project_uid": source["uid"],
            "title": title,
            "forked_by_uid": user["uid"],
        },
        "user",
        user["uid"],
        title,
    )
    audit.record(
        request,
        "project.fork.request",
        user=user,
        target_type="project",
        target_uid=source["uid"],
        target_label=source.get("title"),
        metadata={"new_title": title},
        summary=f"{user['username']} requested a fork of project {source.get('title')}",
        links=[audit.source("project", source["uid"], source.get("title")), audit.job(uid)],
    )
    track_action(user["uid"], "fork")
    return JSONResponse({"uid": uid, "status_url": f"/forks/{uid}"})

@router.post("/delete/{project_slug}")
async def delete_project(request: Request, project_slug: str):
    user = require_user(request)
    return delete_content_item(
        request, "projects", "project", user, project_slug, "/projects"
    )

@router.post("/create")
async def create_project(request: Request, data: Annotated[ProjectForm, Depends(json_or_form(ProjectForm))]):
    user = require_user(request)
    title = data.title.strip()
    description = data.description.strip()
    cover_uid = _hero_attachment_uid(user, data.cover_attachment_uid)
    logo_uid = _hero_attachment_uid(user, data.logo_attachment_uid)

    uid, project_slug = create_content_item(
        "projects",
        "project",
        user,
        {
            "title": title,
            "description": description,
            "release_date": data.release_date or None,
            "demo_date": data.demo_date or None,
            "project_type": data.project_type,
            "platforms": data.platforms.strip(),
            "status": data.status,
            "website_url": data.website_url or None,
            "repo_url": data.repo_url or None,
            "cover_attachment_uid": cover_uid,
            "logo_attachment_uid": logo_uid,
            "is_private": 1 if data.is_private else 0,
            "read_only": 0,
        },
        title,
        XP_PROJECT,
        "First Project",
        description,
        data.attachment_uids,
        request,
    )
    link_attachments([u for u in (cover_uid, logo_uid) if u], "project", uid)
    url = f"/projects/{project_slug}"
    return action_result(
        request, url, data={"uid": uid, "slug": project_slug, "url": url}
    )

@router.post("/edit/{project_slug}")
async def edit_project(
    request: Request, project_slug: str, data: Annotated[ProjectEditForm, Depends(json_or_form(ProjectEditForm))]
):
    user = require_user(request)
    fields = {
        "title": data.title.strip(),
        "description": data.description.strip(),
        "release_date": data.release_date or None,
        "demo_date": data.demo_date or None,
        "project_type": data.project_type,
        "platforms": data.platforms.strip(),
        "status": data.status,
        "website_url": data.website_url or None,
        "repo_url": data.repo_url or None,
    }
    hero_uids = []
    for field in ("cover_attachment_uid", "logo_attachment_uid"):
        uid = _hero_attachment_uid(user, getattr(data, field))
        if uid:
            fields[field] = uid
            hero_uids.append(uid)
    result = edit_content_item(
        request,
        "projects",
        user,
        project_slug,
        fields,
        "/projects",
        target_type="project",
    )
    if hero_uids:
        project = resolve_by_slug(get_table("projects"), project_slug)
        if project and is_owner(project, user):
            link_attachments(hero_uids, "project", project["uid"])
    return result

@router.post("/{project_slug}/screenshots")
async def add_project_screenshots(
    request: Request,
    project_slug: str,
    data: Annotated[ProjectScreenshotsForm, Depends(json_or_form(ProjectScreenshotsForm))],
):
    user = require_user(request)
    project = resolve_by_slug(get_table("projects"), project_slug)
    if not project:
        raise not_found("Project not found")
    if not is_owner(project, user):
        if wants_json(request):
            return json_error(403, "Not allowed")
        return RedirectResponse(url=f"/projects/{project_slug}", status_code=302)
    link_attachments(data.attachment_uids, "project", project["uid"])
    audit.record(
        request,
        "project.screenshots.add",
        user=user,
        target_type="project",
        target_uid=project["uid"],
        target_label=project.get("title"),
        metadata={"attachment_count": len(data.attachment_uids)},
        summary=f"{user['username']} added {len(data.attachment_uids)} screenshot(s) to project {project.get('title')}",
        links=[audit.target("project", project["uid"], project.get("title"))],
    )
    url = f"/projects/{project['slug'] or project['uid']}#screenshots"
    return action_result(
        request,
        url,
        data={"uid": project["uid"], "linked": len(data.attachment_uids), "url": url},
    )


_FLAG_EVENTS = {
    ("is_private", True): "project.visibility.private",
    ("is_private", False): "project.visibility.public",
    ("read_only", True): "project.readonly.enable",
    ("read_only", False): "project.readonly.disable",
}

def _set_project_flag(request: Request, project_slug: str, field: str, value: bool):
    user = require_user(request)
    projects = get_table("projects")
    project = resolve_by_slug(projects, project_slug)
    event_key = _FLAG_EVENTS[(field, bool(value))]
    if not is_owner(project, user):
        audit.record(
            request,
            event_key,
            user=user,
            result="denied",
            target_type="project",
            target_uid=project["uid"] if project else project_slug,
            target_label=project.get("title") if project else project_slug,
            summary=f"{user['username']} denied changing {field} on project {project_slug}",
        )
        if wants_json(request):
            return json_error(403, "Not allowed")
        return RedirectResponse(url=f"/projects/{project_slug}", status_code=302)
    old_value = 1 if project.get(field) else 0
    projects.update({"uid": project["uid"], field: 1 if value else 0}, ["uid"])
    logger.info(
        "project %s %s set to %s by %s",
        project["uid"],
        field,
        bool(value),
        user["username"],
    )
    audit.record(
        request,
        event_key,
        user=user,
        target_type="project",
        target_uid=project["uid"],
        target_label=project.get("title"),
        old_value=old_value,
        new_value=1 if value else 0,
        summary=f"{user['username']} set {field} of project {project.get('title')} to {bool(value)}",
        links=[audit.target("project", project["uid"], project.get("title"))],
    )
    url = f"/projects/{project['slug'] or project['uid']}"
    return action_result(
        request, url, data={"uid": project["uid"], field: bool(value), "url": url}
    )

@router.post("/{project_slug}/private")
async def set_project_private(
    request: Request, project_slug: str, data: Annotated[ProjectFlagForm, Depends(json_or_form(ProjectFlagForm))]
):
    return _set_project_flag(request, project_slug, "is_private", data.value)

@router.post("/{project_slug}/readonly")
async def set_project_readonly(
    request: Request, project_slug: str, data: Annotated[ProjectFlagForm, Depends(json_or_form(ProjectFlagForm))]
):
    return _set_project_flag(request, project_slug, "read_only", data.value)


