#!/usr/bin/env python3
"""Extract file metadata for authorized digital-forensics investigations.

The script walks a file or directory, records filesystem metadata, hashes, MIME
guesses, and optional EXIF tags when Pillow is installed. It does not modify
evidence. Run against mounted read-only evidence when chain of custody matters.
"""

from __future__ import annotations

import argparse
import csv
import hashlib
import mimetypes
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable


def utc_ts(epoch: float) -> str:
    return datetime.fromtimestamp(epoch, tz=timezone.utc).isoformat()


def hash_file(path: Path, algorithm: str, chunk_size: int = 1024 * 1024) -> str:
    digest = hashlib.new(algorithm)
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(chunk_size), b""):
            digest.update(chunk)
    return digest.hexdigest()


def exif_summary(path: Path) -> str:
    try:
        from PIL import Image, ExifTags  # type: ignore
    except Exception:
        return "pillow_not_installed"
    try:
        with Image.open(path) as image:
            exif = image.getexif()
            tags = {ExifTags.TAGS.get(key, str(key)): value for key, value in exif.items()}
            keys = ["DateTimeOriginal", "Make", "Model", "GPSInfo", "Software"]
            return "; ".join(f"{key}={tags[key]}" for key in keys if key in tags) or "no_exif"
    except Exception as exc:
        return f"exif_error:{exc.__class__.__name__}"


def iter_files(root: Path) -> Iterable[Path]:
    if root.is_file():
        yield root
        return
    for directory, _, filenames in os.walk(root):
        for filename in filenames:
            yield Path(directory) / filename


def collect(path: Path, algorithms: list[str]) -> dict[str, str | int]:
    stat = path.stat()
    row: dict[str, str | int] = {
        "path": str(path),
        "size_bytes": stat.st_size,
        "mtime_utc": utc_ts(stat.st_mtime),
        "ctime_utc": utc_ts(stat.st_ctime),
        "atime_utc": utc_ts(stat.st_atime),
        "mime_guess": mimetypes.guess_type(path.name)[0] or "unknown",
        "exif_summary": exif_summary(path),
    }
    for algorithm in algorithms:
        row[f"hash_{algorithm}"] = hash_file(path, algorithm)
    return row


def main() -> int:
    parser = argparse.ArgumentParser(description="Extract metadata from authorized evidence files.")
    parser.add_argument("target", type=Path, help="File or directory to inspect")
    parser.add_argument("--output", type=Path, default=Path("metadata_report.csv"))
    parser.add_argument("--hash", dest="hashes", action="append", default=["sha256"], choices=hashlib.algorithms_available)
    args = parser.parse_args()

    files = [path for path in iter_files(args.target) if path.is_file()]
    if not files:
        raise SystemExit("No files found")

    rows = [collect(path, args.hashes) for path in files]
    with args.output.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=list(rows[0].keys()))
        writer.writeheader()
        writer.writerows(rows)
    print(f"Wrote {len(rows)} metadata rows to {args.output}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
