import "dart:io";

import "package:file_picker/file_picker.dart";
import "package:flutter/material.dart";
import "package:open_filex/open_filex.dart";
import "package:path_provider/path_provider.dart";

import "../i18n.dart";
import "../main.dart";
import "../models.dart";
import "../theme.dart";

/// The extensions an attachment may carry, mirroring attachmentFileTypes in the
/// API's attachments.go. The list is restrictive on purpose: these are scans and
/// photos, and the server rejects anything else.
const _allowedExtensions = ["pdf", "jpg", "jpeg", "png", "webp", "heic"];

/// A form's pending attachment change: a newly picked file, or a request to
/// detach whatever is already there. Both empty means "leave it alone".
class PendingAttachment {
  PlatformFile? file;
  bool remove = false;

  bool get isEmpty => file == null && !remove;
}

/// Applies a form's pending attachment change to the record it has just saved.
///
/// This necessarily runs after the metadata write: the file endpoints address a
/// record that must already exist. The order means a create-with-file is two
/// calls, and the second one failing leaves a saved record with no attachment —
/// which is why callers report it as an attachment error rather than a failed
/// save, because the metadata is already committed.
Future<void> applyAttachment(String path, String id, PendingAttachment pending) async {
  final picked = pending.file;
  if (picked != null) {
    // readAsBytes reads whatever the platform actually handed back — a path, a
    // content:// URI, a blob — so there is no longer a "picked a file but got no
    // bytes" case for the caller to guard.
    final bytes = await picked.readAsBytes();
    await apiClient.uploadAttachment(path, id, bytes, picked.name);
    return;
  }
  if (pending.remove) await apiClient.deleteAttachment(path, id);
}

/// Downloads a record's attachment and hands it to the phone's viewer for that
/// file type. The bytes are fetched through the API Server (never a public URL),
/// then cached to a temp file because the OS viewers open paths, not buffers.
Future<void> openAttachment(
  BuildContext context,
  String path,
  String id,
  String fileName,
) async {
  final messenger = ScaffoldMessenger.of(context);
  try {
    final bytes = await apiClient.getAttachmentBytes(path, id);
    if (bytes == null) {
      messenger.showSnackBar(SnackBar(content: Text(t("errors.noFile"))));
      return;
    }
    final dir = await getTemporaryDirectory();
    final safe = fileName.isEmpty ? "attachment" : fileName.split(RegExp(r"[\\/]")).last;
    final f = File("${dir.path}/$safe");
    await f.writeAsBytes(bytes);
    final res = await OpenFilex.open(f.path);
    if (res.type != ResultType.done) {
      messenger.showSnackBar(SnackBar(
          content: Text(t("errors.openFailed", params: {"error": res.message}))));
    }
  } catch (e) {
    messenger.showSnackBar(
        SnackBar(content: Text(t("errors.openFailed", params: {"error": e}))));
  }
}

/// The attachment picker shared by every form that can carry a file.
///
/// It only collects intent into [pending] — actually moving the bytes is the
/// parent's job (see [applyAttachment]), because the endpoint addresses a record
/// that must already exist.
class AttachmentField extends StatefulWidget {
  /// The saved record, when editing; null while creating. Read for the name of
  /// whatever is already attached.
  final HasAttachment? record;

  /// The collection's API path (e.g. "/service-records"), used to fetch an
  /// existing attachment for viewing.
  final String path;

  /// The saved record's id; null while creating (nothing to view yet).
  final String? recordId;

  final PendingAttachment pending;
  final VoidCallback onChanged;
  /// Blank means the shared wording (attachment.legend / attachment.hint);
  /// callers with a more specific one — "Receipt", "Photo or spec sheet" —
  /// pass it translated.
  final String legend;
  final String hint;

  const AttachmentField({
    super.key,
    required this.path,
    required this.pending,
    required this.onChanged,
    this.record,
    this.recordId,
    this.legend = "",
    this.hint = "",
  });

  @override
  State<AttachmentField> createState() => _AttachmentFieldState();
}

class _AttachmentFieldState extends State<AttachmentField> {
  Future<void> _pick() async {
    final picked = await FilePicker.pickFile(
      type: FileType.custom,
      allowedExtensions: _allowedExtensions,
    );
    if (picked == null) return;
    setState(() {
      widget.pending.file = picked;
      // Picking a replacement supersedes a pending detach.
      widget.pending.remove = false;
    });
    widget.onChanged();
  }

  void _setRemove(bool v) {
    setState(() {
      widget.pending.remove = v;
      if (v) widget.pending.file = null;
    });
    widget.onChanged();
  }

  void _clearPick() {
    setState(() => widget.pending.file = null);
    widget.onChanged();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final muted = theme.textTheme.bodySmall?.color;
    final picked = widget.pending.file;
    final existing = widget.record;
    final hasExisting = existing?.hasFile == true;

    return InputDecorator(
      decoration: InputDecoration(
        labelText: widget.legend.isEmpty ? t("attachment.legend") : widget.legend,
        border: const OutlineInputBorder(),
        helperText: widget.hint.isEmpty ? t("attachment.hint") : widget.hint,
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: [
          Row(
            children: [
              OutlinedButton.icon(
                onPressed: _pick,
                icon: const Icon(Icons.attach_file, size: 18),
                label: Text(
                    t(hasExisting || picked != null ? "attachment.replace" : "attachment.choose")),
              ),
              const SizedBox(width: 8),
              if (picked != null)
                Expanded(
                  child: Text(
                    picked.name,
                    overflow: TextOverflow.ellipsis,
                    style: theme.textTheme.bodySmall,
                  ),
                ),
              if (picked != null)
                IconButton(
                  onPressed: _clearPick,
                  icon: const Icon(Icons.close, size: 18),
                  tooltip: t("attachment.clear"),
                ),
            ],
          ),
          if (picked == null && hasExisting && !widget.pending.remove)
            Padding(
              padding: const EdgeInsets.only(top: 6),
              child: Wrap(
                crossAxisAlignment: WrapCrossAlignment.center,
                spacing: 8,
                children: [
                  Text(t("attachment.attached", params: {"name": existing!.fileName}),
                      style: theme.textTheme.bodySmall),
                  if (widget.recordId != null)
                    _LinkButton(
                      label: t("attachment.view"),
                      onPressed: () => openAttachment(
                          context, widget.path, widget.recordId!, existing.fileName),
                    ),
                  _LinkButton(
                    label: t("common.remove"),
                    color: DriverVault.danger,
                    onPressed: () => _setRemove(true),
                  ),
                ],
              ),
            ),
          if (widget.pending.remove)
            Padding(
              padding: const EdgeInsets.only(top: 6),
              child: Wrap(
                crossAxisAlignment: WrapCrossAlignment.center,
                spacing: 8,
                children: [
                  Text(t("attachment.willBeRemoved"),
                      style: theme.textTheme.bodySmall?.copyWith(color: muted)),
                  _LinkButton(label: t("common.undo"), onPressed: () => _setRemove(false)),
                ],
              ),
            ),
        ],
      ),
    );
  }
}

class _LinkButton extends StatelessWidget {
  final String label;
  final Color? color;
  final VoidCallback onPressed;
  const _LinkButton({required this.label, required this.onPressed, this.color});

  @override
  Widget build(BuildContext context) => InkWell(
        onTap: onPressed,
        child: Text(
          label,
          style: Theme.of(context).textTheme.bodySmall?.copyWith(
                color: color ?? DriverVault.brandOnTint(context),
                fontWeight: FontWeight.w600,
              ),
        ),
      );
}
