using Api.Clients.Api.Contracts;
using CvMatcher.Models.Requests;
using CvMatcher.Models.Responses;
using Models.Requests;
using Models.Settings;
using Api.Services.Contracts;
using Api.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Annotations;
using Shared.Models.Responses;

namespace Api.Controllers;

/// <summary>
/// Proxy endpoints for the CV matcher API. These endpoints forward requests to the internal cv-matcher-api.
/// </summary>
[ApiController]
[Route("api/cv-matcher")]
[EnableRateLimiting("cvMatcher")]
public sealed class CvMatcherController : ControllerBase
{
    private readonly ICvMatcherApi _cvApi;
    private readonly IJobSearchApi _jobSearchApi;
    private readonly ICaptchaVerifier _captcha;
    private readonly FileStorageSettings _fileStorageSettings;
    private readonly JobSearchLinkSettings _jobSearchLinkSettings;
    private readonly IEmailSender _emailSender;
    private readonly ILogger<CvMatcherController> _logger;

    public CvMatcherController(
        ICvMatcherApi cvApi,
        IJobSearchApi jobSearchApi,
        ICaptchaVerifier captcha,
        IOptions<FileStorageSettings> fileStorageSettings,
        IOptions<JobSearchLinkSettings> jobSearchLinkSettings,
        IEmailSender emailSender,
        ILogger<CvMatcherController> logger)
    {
        _cvApi = cvApi;
        _jobSearchApi = jobSearchApi;
        _captcha = captcha;
        _fileStorageSettings = fileStorageSettings.Value;
        _jobSearchLinkSettings = jobSearchLinkSettings.Value;
        _emailSender = emailSender;
        _logger = logger;
    }

    /// <summary>
    /// Upload a CV PDF to the cv-matcher-api.
    /// </summary>
    /// <param name="request">The uploaded CV request.</param>
    /// <param name="ct">Cancellation token.</param>
    [HttpPost("upload")]
    [RequestSizeLimit(8 * 1024 * 1024)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status502BadGateway)]
    [ProducesResponseType(typeof(ErrorResponse), 499)]
    [SwaggerOperation(Summary = "Upload CV", Description = "Proxy upload of a CV PDF to the internal cv-matcher-api.")]
    [SwaggerResponse(StatusCodes.Status200OK, "Upload succeeded")]
    [SwaggerResponse(StatusCodes.Status400BadRequest, "Missing or invalid input")]
    [SwaggerResponse(StatusCodes.Status502BadGateway, "Upstream cv-matcher-api error")]
    public async Task<IActionResult> UploadCv(
        [FromForm] UploadCvRequest request,
        CancellationToken ct)
    {
        if (request.File is null)
        {
            return BadRequest(new ErrorResponse { Error = "Missing CV PDF.", Code = "cv_file_missing" });
        }

        var cv = request.File;
        var gdprConsent = request.GdprConsent;

        try
        {
            var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
            var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, "cv_upload", ct);
            if (!verdict.Success)
            {
                _logger.LogWarning("Captcha verification failed for CV upload. IP={IP}", userIp);
                return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "captcha_verification_failed" });
            }

            if (!gdprConsent) throw new InvalidOperationException("GDPR consent is required.");

            _logger.LogInformation("Proxying CV upload to cv-matcher-api. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}",
                cv.FileName, cv.Length, gdprConsent);

            var stream = cv.OpenReadStream();
            var part = new Refit.StreamPart(stream, cv.FileName, "application/pdf");
            var res = await _cvApi.Upload(part, ct);

            await CacheUploadedCvAsync(cv, res.DocumentId, ct);
            return Ok(res);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            _logger.LogWarning("CV upload proxy request was cancelled by the client.");
            return StatusCode(499, new ErrorResponse { Error = "Request cancelled.", Code = "request_cancelled" });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "CV upload proxy request failed.");
            return StatusCode(StatusCodes.Status502BadGateway, new ErrorResponse { Error = "CV matcher API request failed.", Code = "upstream_request_failed" });
        }
    }

    /// <summary>
    /// Proxy a job matching request to the cv-matcher-api.
    /// </summary>
    /// <param name="request">Job match request payload containing CV document id or job description/url.</param>
    /// <param name="ct">Cancellation token.</param>
    [HttpPost("match-job")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status502BadGateway)]
    [ProducesResponseType(typeof(ErrorResponse), 499)]
    [SwaggerOperation(Summary = "Match job", Description = "Proxy job matching request to the internal cv-matcher-api.")]
    [SwaggerResponse(StatusCodes.Status200OK, "Match succeeded")]
    [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request")]
    [SwaggerResponse(StatusCodes.Status502BadGateway, "Upstream cv-matcher-api error")]
    public async Task<IActionResult> MatchJob([FromBody] JobMatchRequest request, CancellationToken ct)
    {
        try
        {
            var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
            var verdict = await _captcha.VerifyAsync(request.CaptchaToken ?? string.Empty, userIp, "match_job", ct);
            if (!verdict.Success)
            {
                _logger.LogWarning("Captcha verification failed for job match. IP={IP}", userIp);
                return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "captcha_verification_failed" });
            }

            _logger.LogInformation("Proxying job match request to cv-matcher-api. CvDocumentId={CvDocumentId}, HasJobUrl={HasJobUrl}, HasJobDescription={HasJobDescription}",
                request.CvDocumentId,
                !string.IsNullOrWhiteSpace(request.JobUrl),
                !string.IsNullOrWhiteSpace(request.JobDescription));
            var res = await _cvApi.MatchJob(request, ct);
            var attachmentPath = TryGetCachedCvPath(request.CvDocumentId);
            var jobLabel = !string.IsNullOrWhiteSpace(request.JobUrl)
                ? request.JobUrl
                : "Manual job description";

            string? jobSearchLink = null;
            if (!string.IsNullOrWhiteSpace(request.Email) && !string.IsNullOrWhiteSpace(request.CvDocumentId))
            {
                try
                {
                    var tokenResp = await _jobSearchApi.CreateTokenAsync(
                        new CreateJobSearchTokenRequest { CvDocumentId = request.CvDocumentId, Email = request.Email },
                        ct);
                    var baseUrl = _jobSearchLinkSettings.BaseUrl.TrimEnd('/');
                    jobSearchLink = $"{baseUrl}/api/cv-matcher/job-search/start?t={tokenResp.TokenId}";
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Could not create job search token. Email link will be omitted.");
                }
            }

            await _emailSender.SendMatchAsync(
                request.Email,
                SmtpEmailSender.BuildMatchEmailSubject(res.Score, jobLabel),
                SmtpEmailSender.BuildMatchEmailBody(request.CvDocumentId ?? "N/A", res, jobLabel, jobSearchLink),
                attachmentPath,
                ct);

            return Ok(res);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            _logger.LogWarning("Job match proxy request was cancelled by the client.");
            return StatusCode(499, new ErrorResponse { Error = "Request cancelled.", Code = "request_cancelled" });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Job match proxy request failed.");
            return StatusCode(StatusCodes.Status502BadGateway, new ErrorResponse { Error = "CV matcher API request failed.", Code = "upstream_request_failed" });
        }
    }

    [HttpGet("job-search/start")]
    [SwaggerOperation(Summary = "Start job search", Description = "Validates the one-time token and starts the background job search. Returns a simple HTML confirmation page.")]
    public async Task<IActionResult> StartJobSearch([FromQuery] string t, CancellationToken ct)
    {
        try
        {
            var result = await _jobSearchApi.StartSearchAsync(t, ct);
            var html = result.Status switch
            {
                StartJobSearchStatus.Started =>
                    HtmlPage("Job search started", "Your job search has started. Results will be sent to your email shortly."),
                StartJobSearchStatus.AlreadyUsed =>
                    HtmlPage("Link already used", "This job search link has already been used."),
                StartJobSearchStatus.Expired =>
                    HtmlPage("Link expired", "This job search link has expired. Please request a new CV match to get a fresh link."),
                _ =>
                    HtmlPage("Invalid link", "This job search link is not valid.")
            };
            return Content(html, "text/html");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Job search start failed for token {Token}.", t);
            return Content(HtmlPage("Error", "An error occurred. Please try again later."), "text/html");
        }
    }

    private static string HtmlPage(string title, string message) => $$"""
        <!DOCTYPE html>
        <html lang="en">
        <head><meta charset="utf-8"><title>{{title}} - MyAi.ro</title>
        <style>body{font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f5f5f5}
        .card{background:#fff;padding:2rem 3rem;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,.1);text-align:center;max-width:480px}
        h1{font-size:1.4rem;margin-bottom:.5rem}p{color:#555}</style>
        </head>
        <body><div class="card"><h1>{{title}}</h1><p>{{message}}</p></div></body>
        </html>
        """;

    private async Task CacheUploadedCvAsync(IFormFile file, string documentId, CancellationToken ct)
    {
        try
        {
            var storagePath = GetFileStoragePath();
            Directory.CreateDirectory(storagePath);
            var targetPath = BuildCvPath(documentId);
            await using var fileStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true);
            await file.CopyToAsync(fileStream, ct);
            _logger.LogInformation("Cached uploaded CV for email attachment. DocumentId={DocumentId}", documentId);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Could not cache uploaded CV for attachment. DocumentId={DocumentId}", documentId);
        }
    }

    private string? TryGetCachedCvPath(string? cvDocumentId)
    {
        if (string.IsNullOrWhiteSpace(cvDocumentId))
        {
            return null;
        }

        var path = BuildCvPath(cvDocumentId);
        return System.IO.File.Exists(path) ? path : null;
    }

    private string BuildCvPath(string documentId)
    {
        var safeId = string.Concat(documentId.Where(char.IsLetterOrDigit));
        if (string.IsNullOrWhiteSpace(safeId))
        {
            safeId = "cv";
        }

        return Path.Combine(GetFileStoragePath(), $"{safeId}.pdf");
    }

    private string GetFileStoragePath()
    {
        var fileStoragePath = _fileStorageSettings.Path;
        if (!Path.IsPathRooted(fileStoragePath))
        {
            var solutionRoot = Directory.GetCurrentDirectory();
            if (solutionRoot.EndsWith("api", StringComparison.OrdinalIgnoreCase))
            {
                solutionRoot = Directory.GetParent(solutionRoot)?.FullName ?? solutionRoot;
            }
            fileStoragePath = Path.Combine(solutionRoot, fileStoragePath);
        }

        return fileStoragePath;
    }
}
