# ========= 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 mirage.accessor.s3 import S3Accessor
from mirage.cache.index import NULL_INDEX, IndexCacheStore, ResourceType
from mirage.core.s3._client import _client_kwargs, _key, async_session
from mirage.core.timeutil import to_iso_z
from mirage.types import FileStat, FileType, PathSpec
from mirage.utils.errors import enoent
from mirage.utils.filetype import guess_type
from mirage.utils.key_prefix import mount_prefix_of


def _is_not_found(exc: Exception) -> bool:
    if hasattr(exc, "response"):
        code = exc.response.get("Error", {}).get("Code")
        return code in ("404", "NoSuchKey")
    return False


async def stat(accessor: S3Accessor,
               path_spec: PathSpec,
               index: IndexCacheStore = NULL_INDEX) -> FileStat:
    virtual = path_spec.virtual
    original_prefix = mount_prefix_of(path_spec.virtual,
                                      path_spec.resource_path)
    path = path_spec.virtual
    if original_prefix and path.startswith(original_prefix):
        path = path[len(original_prefix):] or "/"

    # A trailing slash ("/s3/csv/") signals the caller treats it as a
    # directory. S3 allows both an object at key "csv" AND a prefix "csv/"
    # to coexist; without this hint head_object would return the file and
    # `ls /s3/csv/` would list the file itself instead of the prefix.
    hints_directory = path.endswith("/")

    stripped = path.strip("/")

    if not stripped:
        return FileStat(name="/", type=FileType.DIRECTORY)

    # Fast path: check the index cache populated by readdir().
    # readdir() stores entries with resource_type="folder" or "file"
    # and file sizes, so stat can return instantly for known paths.
    virtual_key = (original_prefix + "/" +
                   stripped if original_prefix else "/" + stripped)
    lookup = await index.get(virtual_key)
    if lookup.entry is not None:
        entry = lookup.entry
        # S3 "folders" are synthetic common-prefixes with no object,
        # so readdir() records no time or size for them.
        if entry.resource_type == ResourceType.FOLDER:
            return FileStat(name=entry.name, type=FileType.DIRECTORY)
        # TODO: propagate ETag into IndexCacheEntry so this fast
        # path can also carry fingerprint.
        return FileStat(
            name=entry.name,
            size=entry.size,
            modified=entry.remote_time or None,
            type=guess_type(entry.name),
        )
    # If the parent directory was already listed by readdir() but
    # this path is not among its children, it does not exist.
    # This avoids expensive network calls for paths that shells
    # probe speculatively (e.g. .git, HEAD, .hg during cd).
    parent = virtual_key.rsplit("/", 1)[0] or "/"
    parent_listing = await index.list_dir(parent)
    if parent_listing.entries is not None:
        raise enoent(virtual)

    # Slow path: no index cache available, or parent directory not yet
    # listed. Hit the network.
    config = accessor.config
    key = _key(path, config)
    session = async_session(config)
    async with session.client(**_client_kwargs(config)) as client:
        # Try head_object first — works for files. Skipped when the path
        # hints a directory (trailing slash), so a coexisting object of the
        # same name does not shadow the prefix.
        if not hints_directory:
            try:
                resp = await client.head_object(Bucket=config.bucket, Key=key)
                modified = to_iso_z(resp["LastModified"])
                etag_raw = resp.get("ETag", "").strip('"')
                vid_raw = resp.get("VersionId")
                if vid_raw == "null":
                    vid_raw = None
                return FileStat(
                    name=path.rstrip("/").rsplit("/", 1)[-1],
                    size=resp["ContentLength"],
                    modified=modified,
                    type=guess_type(path),
                    fingerprint=etag_raw or None,
                    revision=vid_raw or None,
                    extra={"etag": etag_raw},
                )
            except Exception as exc:
                if not _is_not_found(exc):
                    raise

        # head_object returned 404 (or was skipped) — check if the path is a
        # valid
        # prefix (directory). S3/GCS don't have real directory objects,
        # so we probe with list_objects_v2 using MaxKeys=1.
        pfx = key.rstrip("/") + "/" if key else ""
        resp = await client.list_objects_v2(
            Bucket=config.bucket,
            Prefix=pfx,
            Delimiter="/",
            MaxKeys=1,
        )
        if resp.get("CommonPrefixes") or resp.get("Contents"):
            return FileStat(
                name=path.rstrip("/").rsplit("/", 1)[-1] or "/",
                type=FileType.DIRECTORY,
            )

        raise enoent(virtual)
