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

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 ITemplateService _templates;
    private readonly ILogger<CvMatcherController> _logger;

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

    /// <summary>
    /// Proxies a CV PDF upload to the internal cv-matcher-api for indexing.
    /// Validates the reCAPTCHA token and GDPR consent before forwarding.
    /// Caches the uploaded file locally so it can be attached to the match result email.
    /// </summary>
    /// <param name="request">Multipart form containing the CV PDF, captcha token, and GDPR consent flag.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>
    /// 200 OK with the document ID and cache status from cv-matcher-api;
    /// 400 Bad Request if the file is missing or captcha verification fails;
    /// 499 if the client cancelled the request;
    /// 502 Bad Gateway if the upstream cv-matcher-api call fails.
    /// </returns>
    [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 (Refit.ApiException apiEx) when ((int)apiEx.StatusCode < 500)
        {
            // Forward upstream 4xx errors (e.g. "File is too large", "Only PDF files supported")
            // so the browser can display the actionable message rather than a generic 502.
            var body = await apiEx.GetContentAsAsync<ErrorResponse>();
            _logger.LogWarning("Upstream cv-matcher-api returned {Status} during CV upload: {Error}",
                (int)apiEx.StatusCode, body?.Error);
            return StatusCode((int)apiEx.StatusCode,
                body ?? new ErrorResponse { Error = apiEx.Message, Code = "upstream_error" });
        }
        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>
    /// Proxies a CV-to-job match request to the internal cv-matcher-api.
    /// Validates the reCAPTCHA token, then forwards the request and emails the scored result to the user.
    /// When an email is provided, also creates a one-time job-search token and appends the search link to the email.
    /// </summary>
    /// <param name="request">Match request containing the CV document ID, a job URL or inline description, and an optional recipient email.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>
    /// 200 OK with the <see cref="JobMatchResponse"/> score, strengths, and gaps;
    /// 400 Bad Request if captcha verification fails;
    /// 499 if the client cancelled the request;
    /// 502 Bad Gateway if the upstream cv-matcher-api call fails.
    /// </returns>
    [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" });
            }

            request.ClientIpAddress = userIp;
            _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 language = NormalizeLanguage(request.Language);
            var jobLabel = !string.IsNullOrWhiteSpace(request.JobUrl)
                ? request.JobUrl
                : _emailSender.GetManualJobLabel(language);

            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, Language = language, Keywords = res.Keywords, Location = res.Location, ClientIpAddress = userIp },
                        ct);
                    if (!string.IsNullOrWhiteSpace(tokenResp.TokenId))
                    {
                        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,
                _emailSender.BuildMatchEmailSubject(res.Score, jobLabel, language),
                _emailSender.BuildMatchEmailBody(request.CvDocumentId ?? "N/A", res, jobLabel, language, 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 (Refit.ApiException apiEx) when ((int)apiEx.StatusCode < 500)
        {
            // Forward upstream 4xx errors (e.g. "Could not extract enough job text",
            // "Invalid job URL") so the browser can display the actionable message.
            var body = await apiEx.GetContentAsAsync<ErrorResponse>();
            _logger.LogWarning("Upstream cv-matcher-api returned {Status} during job match: {Error}",
                (int)apiEx.StatusCode, body?.Error);
            return StatusCode((int)apiEx.StatusCode,
                body ?? new ErrorResponse { Error = apiEx.Message, Code = "upstream_error" });
        }
        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" });
        }
    }

    /// <summary>
    /// Validates a one-time job-search token and kicks off the background job search.
    /// Returns a self-contained HTML page intended to be opened directly in the browser via the link in the match email.
    /// </summary>
    /// <param name="t">The one-time UUID token from the job-search link query string.</param>
    /// <param name="ct">Cancellation token.</param>
    /// <returns>
    /// 200 OK with an HTML page indicating whether the search was started, the token was already used, expired, or invalid.
    /// Always returns 200 — error states are communicated via the HTML page content, not the HTTP status code.
    /// </returns>
    [HttpGet("job-search/start")]
    [SwaggerOperation(Summary = "Start job search", Description = "Validates the one-time token and starts the background job search. Returns a self-contained HTML confirmation page.")]
    [SwaggerResponse(StatusCodes.Status200OK, "HTML page returned for all token states (started, already used, expired, invalid)")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<IActionResult> StartJobSearch([FromQuery] string t, CancellationToken ct)
    {
        try
        {
            var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
            var result = await _jobSearchApi.StartSearchAsync(t, new StartJobSearchRequest { ClientIpAddress = userIp }, ct);
            var lang = "en";
            var (title, message) = result.Status switch
            {
                StartJobSearchStatus.Started    => (_templates.Get("html.job-search.started.title",    lang), _templates.Get("html.job-search.started.message",    lang)),
                StartJobSearchStatus.AlreadyUsed => (_templates.Get("html.job-search.already-used.title", lang), _templates.Get("html.job-search.already-used.message", lang)),
                StartJobSearchStatus.Expired    => (_templates.Get("html.job-search.expired.title",    lang), _templates.Get("html.job-search.expired.message",    lang)),
                _                               => (_templates.Get("html.job-search.invalid.title",    lang), _templates.Get("html.job-search.invalid.message",    lang))
            };
            return Content(_templates.Render("html.job-search.shell", "*", ("title", title), ("message", message)), "text/html");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Job search start failed for token {Token}.", t);
            var title = _templates.Get("html.job-search.error.title", "en");
            var message = _templates.Get("html.job-search.error.message", "en");
            return Content(_templates.Render("html.job-search.shell", "*", ("title", title), ("message", message)), "text/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 static string NormalizeLanguage(string? language) =>
        string.IsNullOrWhiteSpace(language) ? "en" : language.ToLowerInvariant().Split('-')[0].Trim();

    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;
    }
}
