# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========

from collections.abc import Awaitable, Callable
from typing import Any

from mirage.accessor.base import Accessor
from mirage.commands.builtin.utils.output import format_optional_records
from mirage.commands.config import CommandOpts
from mirage.commands.registry import command
from mirage.commands.spec import SPECS
from mirage.commands.spec.types import FlagView
from mirage.io.types import ByteSource, IOResult
from mirage.types import PathSpec
from mirage.utils.errors import FS_ERRORS, fs_strerror


def make_rm(
    *,
    resource: str,
    glob_fn: Callable[..., Awaitable[list[PathSpec]]],
    unlink: Callable[..., Awaitable[None]],
) -> Callable[..., Any]:
    """Build a file-only ``rm`` over an index-threaded unlink.

    For backends whose unlink resolves ids through the cache index; the
    factory rm builder calls ``ops.unlink(path)`` without an
    index, so those backends bind this wrapper instead.

    Args:
        resource (str): resource name the command registers under.
        glob_fn (Callable): backend resolve_glob ``(accessor, paths,
            index)``.
        unlink (Callable): backend unlink ``(accessor, path, index)``.
    """

    @command("rm", resource=resource, spec=SPECS["rm"], write=True)
    async def rm(
        accessor: Accessor,
        paths: list[PathSpec],
        texts: list[str],
        opts: CommandOpts,
    ) -> tuple[ByteSource | None, IOResult]:
        if not paths:
            raise ValueError("rm: missing operand")
        fl = FlagView(opts.flags, spec=SPECS["rm"])
        f = fl.as_bool("f")
        v = fl.as_bool("v")
        paths = await glob_fn(accessor, paths, opts.index)
        verbose_parts: list[str] = []
        errors: list[str] = []
        removed: dict[str, ByteSource] = {}
        for p in paths:
            try:
                await unlink(accessor, p, opts.index)
            except FS_ERRORS as exc:
                if f and isinstance(exc, FileNotFoundError):
                    continue
                # GNU rm reports the operand and keeps removing the rest.
                errors.append(
                    f"rm: cannot remove '{p.virtual}': {fs_strerror(exc)}")
                continue
            except ValueError:
                if f:
                    continue
                errors.append(f"rm: cannot remove '{p.virtual}': "
                              "No such file or directory")
                continue
            removed[p.mount_path] = b""
            if v:
                verbose_parts.append(f"removed '{p.virtual}'")
        output = format_optional_records(verbose_parts) if v else None
        stderr = ("\n".join(errors) + "\n").encode() if errors else None
        return output, IOResult(writes=removed,
                                stderr=stderr,
                                exit_code=1 if errors else 0)

    return rm
