# retoor <retoor@molodetz.nl>

import logging
from typing import Annotated

from fastapi import Depends, APIRouter, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse

from devplacepy.database import get_table, resolve_by_slug
from devplacepy.content import is_owner, can_view_project
from devplacepy.models import (
    ProjectFileWriteForm,
    ProjectFileMkdirForm,
    ProjectFileMoveForm,
    ProjectFileDeleteForm,
    ProjectFileReplaceLinesForm,
    ProjectFileInsertLinesForm,
    ProjectFileDeleteLinesForm,
    ProjectFileAppendForm,
)
from devplacepy.responses import respond, action_result, wants_json, json_error
from devplacepy.schemas import ProjectFilesOut
from devplacepy.services.jobs import queue
from devplacepy.seo import base_seo_context
from devplacepy.utils import get_current_user, require_user, not_found, is_admin, track_action, client_ip
from devplacepy import project_files
from devplacepy.project_files import ProjectFileError
from devplacepy.services.audit import record as audit
from devplacepy.dependencies import json_or_form

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

def _audit_file(request, project, user, event_key, path, result="success", metadata=None):
    audit.record(
        request,
        event_key,
        user=user,
        result=result,
        target_type="file",
        target_uid=f"{project['uid']}:{path}",
        target_label=path,
        metadata=metadata,
        summary=f"{(user or {}).get('username', 'someone')} {event_key} {path} in project {project.get('title')}",
        links=[audit.project(project["uid"], project.get("title")), audit.target("file", f"{project['uid']}:{path}", path)],
    )

def _load_project(project_slug: str) -> dict:
    project = resolve_by_slug(get_table("projects"), project_slug)
    if not project:
        raise not_found("Project not found")
    return project

def _load_viewable_project(request: Request, project_slug: str) -> dict:
    project = _load_project(project_slug)
    if not can_view_project(project, get_current_user(request)):
        raise not_found("Project not found")
    return project

def _files_url(project: dict) -> str:
    return f"/projects/{project['slug'] or project['uid']}/files"

def _deny(request: Request, project: dict, user=None, event_key=None, path=""):
    if event_key and user:
        _audit_file(request, project, user, event_key, path, result="denied")
    if wants_json(request):
        return json_error(403, "Not allowed")
    return RedirectResponse(url=_files_url(project), status_code=302)

def _fail(request: Request, project: dict, exc: ProjectFileError, user=None, event_key=None, path=""):
    if event_key:
        result = "denied" if project.get("read_only") else "failure"
        _audit_file(request, project, user, event_key, path, result=result)
    if wants_json(request):
        return json_error(400, str(exc))
    return RedirectResponse(url=_files_url(project), status_code=302)

@router.get("/{project_slug}/files", response_class=HTMLResponse)
async def project_files_page(request: Request, project_slug: str):
    user = get_current_user(request)
    project = _load_viewable_project(request, project_slug)
    files = project_files.list_files(project["uid"])
    seo_ctx = base_seo_context(
        request,
        title=f"Files - {project.get('title', 'Project')}",
        description=f"Files in the {project.get('title', 'project')} project.",
        robots="noindex,follow",
    )
    return respond(
        request,
        "project_files.html",
        {
            **seo_ctx,
            "request": request,
            "user": user,
            "project": project,
            "files": files,
            "is_owner": is_owner(project, user),
            "read_only": bool(project.get("read_only")),
            "is_private": bool(project.get("is_private")),
        },
        model=ProjectFilesOut,
    )

@router.get("/{project_slug}/files/raw")
async def project_file_raw(request: Request, project_slug: str, path: str):
    project = _load_viewable_project(request, project_slug)
    try:
        return JSONResponse(project_files.read_file(project["uid"], path))
    except ProjectFileError as exc:
        return json_error(404, str(exc))

@router.post("/{project_slug}/files/zip")
async def project_files_zip(request: Request, project_slug: str, path: str = ""):
    project = _load_viewable_project(request, project_slug)
    user = get_current_user(request)
    subpath = ""
    name = project.get("title") or "project"
    if path:
        try:
            subpath = project_files.normalize_path(path)
        except ProjectFileError as exc:
            return json_error(400, str(exc))
        if project_files.get_node(project["uid"], subpath) is None:
            return json_error(404, "Path not found")
        name = subpath.rsplit("/", 1)[-1]
    owner_kind, owner_id = (
        ("user", user["uid"]) if user else ("guest", client_ip(request, default="anon"))
    )
    uid = queue.enqueue(
        "zip",
        {
            "source": {
                "type": "project_tree",
                "project_uid": project["uid"],
                "path": subpath,
            }
        },
        owner_kind,
        owner_id,
        name,
    )
    audit.record(
        request,
        "files.zip.request",
        user=user,
        actor_kind="user" if user else "guest",
        target_type="project",
        target_uid=project["uid"],
        target_label=project.get("title"),
        metadata={"subpath": subpath},
        summary=f"requested a zip of {subpath or 'whole tree'} in project {project.get('title')}",
        links=[audit.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.get("/{project_slug}/files/lines")
async def project_file_lines(
    request: Request, project_slug: str, path: str, start: int = 1, end: int = -1
):
    project = _load_viewable_project(request, project_slug)
    try:
        return JSONResponse(
            project_files.read_lines(
                project["uid"], path, start, None if end < 0 else end
            )
        )
    except ProjectFileError as exc:
        return json_error(404, str(exc))

@router.post("/{project_slug}/files/write")
async def project_file_write(
    request: Request, project_slug: str, data: Annotated[ProjectFileWriteForm, Depends(json_or_form(ProjectFileWriteForm))]
):
    user = require_user(request)
    project = _load_project(project_slug)
    if not is_owner(project, user):
        return _deny(request, project, user, "file.write", data.path)
    existed = project_files.get_node(project["uid"], data.path) is not None
    write_event = "file.write.overwrite" if existed else "file.write.create"
    try:
        node = project_files.write_text_file(
            project["uid"], user, data.path, data.content
        )
    except ProjectFileError as exc:
        return _fail(request, project, exc, user, write_event, data.path)
    logger.info("project %s file written: %s", project["uid"], node["path"])
    _audit_file(request, project, user, write_event, node["path"])
    if not existed:
        track_action(user["uid"], "project_file")
    return action_result(
        request, _files_url(project), data=project_files.node_to_dict(node)
    )

def _edit(request: Request, project: dict, mutate, user=None, event_key=None, metadata=None) -> JSONResponse:
    try:
        node = mutate()
    except ProjectFileError as exc:
        return _fail(request, project, exc, user, event_key)
    logger.info("project %s file edited: %s", project["uid"], node["path"])
    if event_key:
        _audit_file(request, project, user, event_key, node["path"], metadata=metadata)
    return action_result(
        request, _files_url(project), data=project_files.node_to_dict(node)
    )

@router.post("/{project_slug}/files/replace-lines")
async def project_file_replace_lines(
    request: Request,
    project_slug: str,
    data: Annotated[ProjectFileReplaceLinesForm, Depends(json_or_form(ProjectFileReplaceLinesForm))],
):
    user = require_user(request)
    project = _load_project(project_slug)
    if not is_owner(project, user):
        return _deny(request, project, user, "file.replace_lines", data.path)
    return _edit(
        request,
        project,
        lambda: project_files.replace_lines(
            project["uid"], data.path, data.start, data.end, data.content
        ),
        user,
        "file.replace_lines",
        {"line_start": data.start, "line_end": data.end},
    )

@router.post("/{project_slug}/files/insert-lines")
async def project_file_insert_lines(
    request: Request,
    project_slug: str,
    data: Annotated[ProjectFileInsertLinesForm, Depends(json_or_form(ProjectFileInsertLinesForm))],
):
    user = require_user(request)
    project = _load_project(project_slug)
    if not is_owner(project, user):
        return _deny(request, project, user, "file.insert_lines", data.path)
    return _edit(
        request,
        project,
        lambda: project_files.insert_lines(
            project["uid"], data.path, data.at, data.content
        ),
        user,
        "file.insert_lines",
        {"insert_at": data.at},
    )

@router.post("/{project_slug}/files/delete-lines")
async def project_file_delete_lines(
    request: Request,
    project_slug: str,
    data: Annotated[ProjectFileDeleteLinesForm, Depends(json_or_form(ProjectFileDeleteLinesForm))],
):
    user = require_user(request)
    project = _load_project(project_slug)
    if not is_owner(project, user):
        return _deny(request, project, user, "file.delete_lines", data.path)
    return _edit(
        request,
        project,
        lambda: project_files.delete_lines(
            project["uid"], data.path, data.start, data.end
        ),
        user,
        "file.delete_lines",
        {"line_start": data.start, "line_end": data.end},
    )

@router.post("/{project_slug}/files/append")
async def project_file_append(
    request: Request, project_slug: str, data: Annotated[ProjectFileAppendForm, Depends(json_or_form(ProjectFileAppendForm))]
):
    user = require_user(request)
    project = _load_project(project_slug)
    if not is_owner(project, user):
        return _deny(request, project, user, "file.append", data.path)
    return _edit(
        request,
        project,
        lambda: project_files.append_lines(project["uid"], data.path, data.content),
        user,
        "file.append",
    )

@router.post("/{project_slug}/files/upload")
async def project_file_upload(request: Request, project_slug: str):
    user = require_user(request)
    project = _load_project(project_slug)
    if not is_owner(project, user):
        return _deny(request, project, user, "file.upload", "")
    form = await request.form()
    file = form.get("file")
    if not file or not hasattr(file, "filename") or not file.filename:
        return _fail(request, project, ProjectFileError("no file provided"))
    target_dir = str(form.get("path", "") or "")
    try:
        content = await file.read()
        node = project_files.store_upload(
            project["uid"], user, target_dir, file.filename, content
        )
    except ProjectFileError as exc:
        return _fail(request, project, exc, user, "file.upload", target_dir)
    logger.info("project %s file uploaded: %s", project["uid"], node["path"])
    _audit_file(
        request, project, user, "file.upload", node["path"],
        metadata={"size": len(content), "filename": file.filename},
    )
    return action_result(
        request, _files_url(project), data=project_files.node_to_dict(node)
    )

@router.post("/{project_slug}/files/mkdir")
async def project_file_mkdir(
    request: Request, project_slug: str, data: Annotated[ProjectFileMkdirForm, Depends(json_or_form(ProjectFileMkdirForm))]
):
    user = require_user(request)
    project = _load_project(project_slug)
    if not is_owner(project, user):
        return _deny(request, project, user, "dir.create", data.path)
    try:
        node = project_files.make_dir(project["uid"], user, data.path)
    except ProjectFileError as exc:
        return _fail(request, project, exc, user, "dir.create", data.path)
    _audit_file(request, project, user, "dir.create", node["path"])
    return action_result(
        request, _files_url(project), data=project_files.node_to_dict(node)
    )

@router.post("/{project_slug}/files/move")
async def project_file_move(
    request: Request, project_slug: str, data: Annotated[ProjectFileMoveForm, Depends(json_or_form(ProjectFileMoveForm))]
):
    user = require_user(request)
    project = _load_project(project_slug)
    if not is_owner(project, user):
        return _deny(request, project, user, "file.move", data.from_path)
    try:
        node = project_files.move_node(
            project["uid"], user, data.from_path, data.to_path
        )
    except ProjectFileError as exc:
        return _fail(request, project, exc, user, "file.move", data.from_path)
    audit.record(
        request,
        "file.move",
        user=user,
        target_type="file",
        target_uid=f"{project['uid']}:{data.to_path}",
        target_label=data.to_path,
        summary=f"{user['username']} moved {data.from_path} to {data.to_path} in project {project.get('title')}",
        links=[
            audit.project(project["uid"], project.get("title")),
            audit.source("file", f"{project['uid']}:{data.from_path}", data.from_path),
            audit.destination("file", f"{project['uid']}:{data.to_path}", data.to_path),
        ],
    )
    return action_result(
        request, _files_url(project), data=project_files.node_to_dict(node)
    )

@router.post("/{project_slug}/files/delete")
async def project_file_delete(
    request: Request, project_slug: str, data: Annotated[ProjectFileDeleteForm, Depends(json_or_form(ProjectFileDeleteForm))]
):
    user = require_user(request)
    project = _load_project(project_slug)
    if not (is_owner(project, user) or is_admin(user)):
        return _deny(request, project, user, "file.delete", data.path)
    try:
        project_files.delete_node(project["uid"], data.path, deleted_by=user["uid"])
    except ProjectFileError as exc:
        return _fail(request, project, exc, user, "file.delete", data.path)
    _audit_file(request, project, user, "file.delete", data.path)
    return action_result(request, _files_url(project), data={"path": data.path})
