import fp from "fastify-plugin";
import type { FastifyInstance } from "fastify";
import { createHmac, randomUUID } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";

/**
 * Lagringsabstraktion (spec §53). Två lägen:
 *
 * - mock (dev/test): "presignade" URL:er pekar på API:ts egna PUT/GET-endpoints
 *   och filer lagras under .data/s3/. Hela flödet (app → signed upload →
 *   worker läser) fungerar utan AWS.
 * - aws (staging/produktion): RIKTIGA S3 presigned URLs via @aws-sdk/client-s3
 *   + @aws-sdk/s3-request-presigner. Fungerar mot AWS S3 och alla
 *   S3-kompatibla lagringar (MinIO, Cloudflare R2, Hetzner …) via S3_ENDPOINT.
 *   Verifierad med riktig rundtur (presign → PUT → presign → GET) mot MinIO.
 *
 * Nyckelstruktur enligt spec §53: users/, fridge-scans/, pantry-scans/,
 * meal-scans/, receipts/, product-images/, recipe-images/, temporary/.
 */
import {
  GetObjectCommand,
  HeadBucketCommand,
  PutObjectCommand,
  S3Client,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

export interface PresignedUpload {
  key: string;
  uploadUrl: string;
  method: "PUT";
  headers: Record<string, string>;
  expiresAt: string;
}

export interface StorageService {
  presignUpload(prefix: string, contentType: string): Promise<PresignedUpload>;
  /** URL som workern/AAMOS kan läsa bilden från. */
  getReadUrl(key: string): Promise<string>;
  putObject(key: string, data: Buffer, contentType: string): Promise<void>;
  getObject(key: string): Promise<Buffer | null>;
}

declare module "fastify" {
  interface FastifyInstance {
    storage: StorageService;
  }
}

const MOCK_ROOT = path.resolve(process.cwd(), ".data/s3");

class MockStorage implements StorageService {
  constructor(
    private readonly baseUrl: string,
    private readonly signingSecret: string,
  ) {}

  private sign(key: string): string {
    return createHmac("sha256", this.signingSecret).update(key).digest("hex").slice(0, 32);
  }

  async presignUpload(prefix: string, contentType: string): Promise<PresignedUpload> {
    const ext = contentType.split("/")[1] ?? "bin";
    const key = `${prefix}/${randomUUID()}.${ext}`;
    const sig = this.sign(key);
    return {
      key,
      uploadUrl: `${this.baseUrl}/v1/mock-s3/${encodeURIComponent(key)}?sig=${sig}`,
      method: "PUT",
      headers: { "content-type": contentType },
      expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
    };
  }

  async getReadUrl(key: string): Promise<string> {
    return `${this.baseUrl}/v1/mock-s3/${encodeURIComponent(key)}?sig=${this.sign(key)}`;
  }

  async putObject(key: string, data: Buffer, _contentType?: string): Promise<void> {
    const filePath = path.join(MOCK_ROOT, key);
    await mkdir(path.dirname(filePath), { recursive: true });
    await writeFile(filePath, data);
  }

  async getObject(key: string): Promise<Buffer | null> {
    try {
      return await readFile(path.join(MOCK_ROOT, key));
    } catch {
      return null;
    }
  }

  verifySignature(key: string, sig: string): boolean {
    return this.sign(key) === sig;
  }
}

/** Riktig S3 (AWS eller S3-kompatibel via S3_ENDPOINT). */
export class AwsStorage implements StorageService {
  private readonly client: S3Client;

  constructor(
    private readonly bucket: string,
    region: string,
    endpoint: string | undefined,
    accessKeyId: string | undefined,
    secretAccessKey: string | undefined,
  ) {
    this.client = new S3Client({
      region,
      ...(endpoint ? { endpoint, forcePathStyle: true } : {}),
      ...(accessKeyId && secretAccessKey ? { credentials: { accessKeyId, secretAccessKey } } : {}), // annars IAM-roll/instansprofil (rekommenderat i AWS)
    });
  }

  /** Snabb verifiering vid uppstart – hellre högljutt fel än trasiga uppladdningar. */
  async healthCheck(): Promise<void> {
    await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }));
  }

  async presignUpload(prefix: string, contentType: string): Promise<PresignedUpload> {
    const ext = contentType.split("/")[1] ?? "bin";
    const key = `${prefix}/${randomUUID()}.${ext}`;
    const uploadUrl = await getSignedUrl(
      this.client,
      new PutObjectCommand({ Bucket: this.bucket, Key: key, ContentType: contentType }),
      { expiresIn: 15 * 60 },
    );
    return {
      key,
      uploadUrl,
      method: "PUT",
      headers: { "content-type": contentType },
      expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
    };
  }

  async getReadUrl(key: string): Promise<string> {
    return getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.bucket, Key: key }), {
      expiresIn: 60 * 60,
    });
  }

  async putObject(key: string, data: Buffer, contentType: string): Promise<void> {
    await this.client.send(
      new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: data, ContentType: contentType }),
    );
  }

  async getObject(key: string): Promise<Buffer | null> {
    try {
      const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
      const bytes = await res.Body?.transformToByteArray();
      return bytes ? Buffer.from(bytes) : null;
    } catch {
      return null;
    }
  }
}

export const storagePlugin = fp(async (app: FastifyInstance) => {
  let storage: StorageService;
  if (app.config.S3_MODE === "aws") {
    const aws = new AwsStorage(
      app.config.S3_BUCKET,
      app.config.S3_REGION,
      app.config.S3_ENDPOINT || undefined,
      app.config.S3_ACCESS_KEY_ID || undefined,
      app.config.S3_SECRET_ACCESS_KEY || undefined,
    );
    // Verifiera bucket-åtkomst vid uppstart – produktion ska aldrig starta halvtrasig.
    try {
      await aws.healthCheck();
      app.log.info(`S3: riktig lagring aktiv (bucket: ${app.config.S3_BUCKET})`);
    } catch (err) {
      if (app.config.NODE_ENV === "production") {
        throw new Error(
          `S3-bucketen "${app.config.S3_BUCKET}" är inte nåbar: ${(err as Error).message}`,
        );
      }
      app.log.error(
        `S3-bucketen "${app.config.S3_BUCKET}" svarar inte (${(err as Error).message}) – kontrollera S3_*-värdena.`,
      );
    }
    storage = aws;
    app.decorate("storage", storage);
    return; // riktiga presignade URL:er – inga lokala S3-endpoints behövs
  }

  const mockStorage = new MockStorage(
    app.config.API_BASE_URL,
    app.config.ENTITLEMENT_SIGNING_SECRET,
  );
  storage = mockStorage;
  app.decorate("storage", storage);

  // Mock-S3-endpoints (endast i mock-läge)
  app.addContentTypeParser(
    ["image/jpeg", "image/png", "image/webp", "image/heic", "application/octet-stream"],
    { parseAs: "buffer", bodyLimit: 15 * 1024 * 1024 },
    (_req, body, done) => done(null, body),
  );

  app.put<{ Params: { key: string }; Querystring: { sig?: string } }>(
    "/v1/mock-s3/:key",
    { config: { rateLimit: false } },
    async (req, reply) => {
      const key = decodeURIComponent(req.params.key);
      if (!mockStorage.verifySignature(key, req.query.sig ?? "")) {
        return reply
          .status(403)
          .send({ error: { code: "BAD_SIGNATURE", message: "Ogiltig signatur" } });
      }
      const body = req.body as Buffer | undefined;
      if (!body || body.length === 0) {
        return reply.status(400).send({ error: { code: "EMPTY_BODY", message: "Tom fil" } });
      }
      await mockStorage.putObject(
        key,
        body,
        req.headers["content-type"] ?? "application/octet-stream",
      );
      return reply.status(200).send({ ok: true, key });
    },
  );

  app.get<{ Params: { key: string }; Querystring: { sig?: string } }>(
    "/v1/mock-s3/:key",
    { config: { rateLimit: false } },
    async (req, reply) => {
      const key = decodeURIComponent(req.params.key);
      if (!mockStorage.verifySignature(key, req.query.sig ?? "")) {
        return reply
          .status(403)
          .send({ error: { code: "BAD_SIGNATURE", message: "Ogiltig signatur" } });
      }
      const data = await mockStorage.getObject(key);
      if (!data)
        return reply
          .status(404)
          .send({ error: { code: "NOT_FOUND", message: "Filen finns inte" } });
      return reply.header("content-type", "application/octet-stream").send(data);
    },
  );
});
