import logging
import os
from pathlib import Path

from django.conf import settings

from documents.models import Document
from documents.templating.filepath import is_safe_relative_path
from documents.templating.filepath import validate_filepath_template_and_render
from documents.templating.utils import convert_format_str_to_template_format

logger = logging.getLogger("paperless.filehandling")


class UnsafeFilePathError(Exception):
    """
    Raised when a path generated for a document would land outside of its root.
    """


def validate_path_in_root(path: Path, root: Path) -> None:
    """
    Ensures the given absolute path is contained within root, the
    equivalent guard for the later move.
    """
    if not path.resolve().is_relative_to(root.resolve()):
        msg = f"Refusing to write file outside of root {root}: {path}."
        logger.warning(msg)
        raise UnsafeFilePathError(msg)


def create_source_path_directory(source_path: Path) -> None:
    source_path.parent.mkdir(parents=True, exist_ok=True)


def delete_empty_directories(directory: Path, root: Path) -> None:
    if not directory.is_dir():
        return

    if not directory.is_relative_to(root):
        # don't do anything outside our originals folder.

        # append os.path.set so that we avoid these cases:
        #   directory = /home/originals2/test
        #   root = /home/originals ("/" gets appended and startswith fails)
        return

    # Go up in the directory hierarchy and try to delete all directories
    while directory != root:
        if not list(directory.iterdir()):
            # it's empty
            try:
                directory.rmdir()
            except OSError:
                # whatever. empty directories aren't that bad anyway.
                return
        else:
            # it's not empty.
            return

        # go one level up
        directory = directory.parent


def generate_unique_filename(doc, *, archive_filename=False) -> Path:
    """
    Generates a unique filename for doc in settings.ORIGINALS_DIR.

    The returned filename is guaranteed to be either the current filename
    of the document if unchanged, or a new filename that does not correspondent
    to any existing files. The function will append _01, _02, etc to the
    filename before the extension to avoid conflicts.

    If archive_filename is True, return a unique archive filename instead.

    """
    if archive_filename:
        old_filename: Path | None = (
            Path(doc.archive_filename) if doc.archive_filename else None
        )
        root = settings.ARCHIVE_DIR
    else:
        old_filename = Path(doc.filename) if doc.filename else None
        root = settings.ORIGINALS_DIR

    base_filename = generate_filename(doc, archive_filename=archive_filename)

    # If generating archive filenames, try to make a name that is similar to
    # the original filename first.

    if archive_filename and doc.filename:
        # Try to create a simple PDF version based on the original filename
        # but preserve any directory structure from the template
        if str(base_filename.parent) != ".":
            # Has directory structure, preserve it
            simple_pdf_name = base_filename.parent / (Path(doc.filename).stem + ".pdf")
        else:
            # No directory structure
            simple_pdf_name = Path(Path(doc.filename).stem + ".pdf")

        if simple_pdf_name == old_filename or not (root / simple_pdf_name).exists():
            return simple_pdf_name

    file_extension = ".pdf" if archive_filename else doc.file_type
    filename_stem = base_filename.name.removesuffix(file_extension)
    counter = 0

    while True:
        new_filename = base_filename
        if counter:
            new_filename = base_filename.with_name(
                f"{filename_stem}_{counter:02}{file_extension}",
            )

        if new_filename == old_filename:
            # still the same as before.
            return new_filename

        if (root / new_filename).exists():
            counter += 1
        else:
            return new_filename


def format_filename(document: Document, template_str: str) -> str | None:
    rendered_filename = validate_filepath_template_and_render(
        template_str,
        document,
    )
    if rendered_filename is None:
        return None

    # Apply this setting.  It could become a filter in the future (or users could use |default)
    if settings.FILENAME_FORMAT_REMOVE_NONE:
        rendered_filename = rendered_filename.replace("/-none-/", "/")
        rendered_filename = rendered_filename.replace(" -none-", "")
        rendered_filename = rendered_filename.replace("-none-", "")
        rendered_filename = rendered_filename.strip(os.sep)

    rendered_filename = rendered_filename.replace(
        "-none-",
        "none",
    )  # backward compatibility

    # Validate again after remove none
    if not is_safe_relative_path(rendered_filename):
        logger.warning(
            "Filename became unsafe after placeholder removal, "
            "falling back to default naming",
        )
        return None

    return rendered_filename


def generate_filename(
    doc: Document,
    *,
    counter=0,
    archive_filename=False,
    use_format=True,
) -> Path:
    # version docs use the root document for formatting, just with a suffix
    context_doc = doc if doc.root_document_id is None else doc.root_document
    version_suffix = (
        f"_v{doc.version_index}"
        if doc.root_document_id is not None and doc.version_index is not None
        else ""
    )
    base_path: Path | None = None

    # Determine the source of the format string
    if use_format:
        if context_doc.storage_path is not None:
            filename_format = context_doc.storage_path.path
        elif settings.FILENAME_FORMAT is not None:
            # Maybe convert old to new style
            filename_format = convert_format_str_to_template_format(
                settings.FILENAME_FORMAT,
            )
        else:
            filename_format = None
    else:
        filename_format = None

    # If we have one, render it
    if filename_format is not None:
        rendered_path: str | None = format_filename(context_doc, filename_format)
        if rendered_path:
            base_path = Path(rendered_path)

    counter_str = f"_{counter:02}" if counter else ""
    filetype_str = ".pdf" if archive_filename else doc.file_type

    if base_path:
        # Split the path into directory and filename parts
        directory = base_path.parent
        # Use the full name (not just stem) as the base filename
        base_filename = base_path.name

        # Build the final filename with counter and filetype
        final_filename = f"{base_filename}{version_suffix}{counter_str}{filetype_str}"

        # If we have a directory component, include it
        if str(directory) != ".":
            full_path = directory / final_filename
        else:
            full_path = Path(final_filename)
    else:
        # No template, use document ID
        final_filename = (
            f"{context_doc.pk:07}{version_suffix}{counter_str}{filetype_str}"
        )
        full_path = Path(final_filename)

    return full_path
